summaryrefslogtreecommitdiffstats
path: root/js/misc
diff options
context:
space:
mode:
Diffstat (limited to 'js/misc')
-rw-r--r--js/misc/config.js.in19
-rw-r--r--js/misc/extensionUtils.js268
-rw-r--r--js/misc/fileUtils.js117
-rw-r--r--js/misc/gnomeSession.js37
-rw-r--r--js/misc/history.js109
-rw-r--r--js/misc/ibusManager.js276
-rw-r--r--js/misc/inputMethod.js285
-rw-r--r--js/misc/introspect.js242
-rw-r--r--js/misc/jsParse.js237
-rw-r--r--js/misc/keyboardManager.js159
-rw-r--r--js/misc/loginManager.js243
-rw-r--r--js/misc/meson.build15
-rw-r--r--js/misc/modemManager.js298
-rw-r--r--js/misc/objectManager.js285
-rw-r--r--js/misc/params.js28
-rw-r--r--js/misc/parentalControlsManager.js146
-rw-r--r--js/misc/permissionStore.js16
-rw-r--r--js/misc/smartcardManager.js116
-rw-r--r--js/misc/systemActions.js457
-rw-r--r--js/misc/util.js437
-rw-r--r--js/misc/weather.js320
21 files changed, 4110 insertions, 0 deletions
diff --git a/js/misc/config.js.in b/js/misc/config.js.in
new file mode 100644
index 0000000..e54e280
--- /dev/null
+++ b/js/misc/config.js.in
@@ -0,0 +1,19 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+/* The name of this package (not localized) */
+var PACKAGE_NAME = '@PACKAGE_NAME@';
+/* The version of this package */
+var PACKAGE_VERSION = '@PACKAGE_VERSION@';
+/* 1 if gnome-bluetooth is available, 0 otherwise */
+var HAVE_BLUETOOTH = @HAVE_BLUETOOTH@;
+/* 1 if networkmanager is available, 0 otherwise */
+var HAVE_NETWORKMANAGER = @HAVE_NETWORKMANAGER@;
+/* gettext package */
+var GETTEXT_PACKAGE = '@GETTEXT_PACKAGE@';
+/* locale dir */
+var LOCALEDIR = '@datadir@/locale';
+/* other standard directories */
+var LIBEXECDIR = '@libexecdir@';
+var PKGDATADIR = '@datadir@/@PACKAGE_NAME@';
+/* g-i package versions */
+var LIBMUTTER_API_VERSION = '@LIBMUTTER_API_VERSION@'
diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js
new file mode 100644
index 0000000..e9697d4
--- /dev/null
+++ b/js/misc/extensionUtils.js
@@ -0,0 +1,268 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported ExtensionState, ExtensionType, getCurrentExtension,
+ getSettings, initTranslations, openPrefs, isOutOfDate,
+ installImporter, serializeExtension, deserializeExtension */
+
+// Common utils for the extension system and the extension
+// preferences tool
+
+const { Gio, GLib } = imports.gi;
+
+const Gettext = imports.gettext;
+const Lang = imports.lang;
+
+const Config = imports.misc.config;
+
+var ExtensionType = {
+ SYSTEM: 1,
+ PER_USER: 2,
+};
+
+var ExtensionState = {
+ ENABLED: 1,
+ DISABLED: 2,
+ ERROR: 3,
+ OUT_OF_DATE: 4,
+ DOWNLOADING: 5,
+ INITIALIZED: 6,
+
+ // Used as an error state for operations on unknown extensions,
+ // should never be in a real extensionMeta object.
+ UNINSTALLED: 99,
+};
+
+const SERIALIZED_PROPERTIES = [
+ 'type',
+ 'state',
+ 'path',
+ 'error',
+ 'hasPrefs',
+ 'hasUpdate',
+ 'canChange',
+];
+
+/**
+ * getCurrentExtension:
+ *
+ * @returns {?object} - The current extension, or null if not called from
+ * an extension.
+ */
+function getCurrentExtension() {
+ let stack = new Error().stack.split('\n');
+ let extensionStackLine;
+
+ // Search for an occurrence of an extension stack frame
+ // Start at 1 because 0 is the stack frame of this function
+ for (let i = 1; i < stack.length; i++) {
+ if (stack[i].includes('/gnome-shell/extensions/')) {
+ extensionStackLine = stack[i];
+ break;
+ }
+ }
+ if (!extensionStackLine)
+ return null;
+
+ // The stack line is like:
+ // init([object Object])@/home/user/data/gnome-shell/extensions/u@u.id/prefs.js:8
+ //
+ // In the case that we're importing from
+ // module scope, the first field is blank:
+ // @/home/user/data/gnome-shell/extensions/u@u.id/prefs.js:8
+ let match = new RegExp('@(.+):\\d+').exec(extensionStackLine);
+ if (!match)
+ return null;
+
+ // local import, as the module is used from outside the gnome-shell process
+ // as well (not this function though)
+ let extensionManager = imports.ui.main.extensionManager;
+
+ let path = match[1];
+ let file = Gio.File.new_for_path(path);
+
+ // Walk up the directory tree, looking for an extension with
+ // the same UUID as a directory name.
+ while (file != null) {
+ let extension = extensionManager.lookup(file.get_basename());
+ if (extension !== undefined)
+ return extension;
+ file = file.get_parent();
+ }
+
+ return null;
+}
+
+/**
+ * initTranslations:
+ * @param {string=} domain - the gettext domain to use
+ *
+ * Initialize Gettext to load translations from extensionsdir/locale.
+ * If @domain is not provided, it will be taken from metadata['gettext-domain']
+ */
+function initTranslations(domain) {
+ let extension = getCurrentExtension();
+
+ if (!extension)
+ throw new Error('initTranslations() can only be called from extensions');
+
+ domain = domain || extension.metadata['gettext-domain'];
+
+ // Expect USER extensions to have a locale/ subfolder, otherwise assume a
+ // SYSTEM extension that has been installed in the same prefix as the shell
+ let localeDir = extension.dir.get_child('locale');
+ if (localeDir.query_exists(null))
+ Gettext.bindtextdomain(domain, localeDir.get_path());
+ else
+ Gettext.bindtextdomain(domain, Config.LOCALEDIR);
+}
+
+/**
+ * getSettings:
+ * @param {string=} schema - the GSettings schema id
+ * @returns {Gio.Settings} - a new settings object for @schema
+ *
+ * Builds and returns a GSettings schema for @schema, using schema files
+ * in extensionsdir/schemas. If @schema is omitted, it is taken from
+ * metadata['settings-schema'].
+ */
+function getSettings(schema) {
+ let extension = getCurrentExtension();
+
+ if (!extension)
+ throw new Error('getSettings() can only be called from extensions');
+
+ schema = schema || extension.metadata['settings-schema'];
+
+ const GioSSS = Gio.SettingsSchemaSource;
+
+ // Expect USER extensions to have a schemas/ subfolder, otherwise assume a
+ // SYSTEM extension that has been installed in the same prefix as the shell
+ let schemaDir = extension.dir.get_child('schemas');
+ let schemaSource;
+ if (schemaDir.query_exists(null)) {
+ schemaSource = GioSSS.new_from_directory(schemaDir.get_path(),
+ GioSSS.get_default(),
+ false);
+ } else {
+ schemaSource = GioSSS.get_default();
+ }
+
+ let schemaObj = schemaSource.lookup(schema, true);
+ if (!schemaObj)
+ throw new Error(`Schema ${schema} could not be found for extension ${extension.metadata.uuid}. Please check your installation`);
+
+ return new Gio.Settings({ settings_schema: schemaObj });
+}
+
+/**
+ * openPrefs:
+ *
+ * Open the preference dialog of the current extension
+ */
+function openPrefs() {
+ const extension = getCurrentExtension();
+
+ if (!extension)
+ throw new Error('openPrefs() can only be called from extensions');
+
+ try {
+ const extensionManager = imports.ui.main.extensionManager;
+ extensionManager.openExtensionPrefs(extension.uuid, '', {});
+ } catch (e) {
+ if (e.name === 'ImportError')
+ throw new Error('openPrefs() cannot be called from preferences');
+ logError(e, 'Failed to open extension preferences');
+ }
+}
+
+/**
+ * versionCheck:
+ * @param {string[]} required - an array of versions we're compatible with
+ * @param {string} current - the version we have
+ * @returns {bool} - true if @current is compatible with @required
+ *
+ * Check if a component is compatible for an extension.
+ * @required is an array, and at least one version must match.
+ * @current must be in the format <major>.<minor>.<point>.<micro>
+ * <micro> is always ignored
+ * <point> is ignored if <minor> is even (so you can target the
+ * whole stable release)
+ * <minor> and <major> must match
+ * Each target version must be at least <major> and <minor>
+ */
+function versionCheck(required, current) {
+ let currentArray = current.split('.');
+ let major = currentArray[0];
+ let minor = currentArray[1];
+ let point = currentArray[2];
+ for (let i = 0; i < required.length; i++) {
+ let requiredArray = required[i].split('.');
+ if (requiredArray[0] == major &&
+ requiredArray[1] == minor &&
+ ((requiredArray[2] === undefined && parseInt(minor) % 2 == 0) ||
+ requiredArray[2] == point))
+ return true;
+ }
+ return false;
+}
+
+function isOutOfDate(extension) {
+ if (!versionCheck(extension.metadata['shell-version'], Config.PACKAGE_VERSION))
+ return true;
+
+ return false;
+}
+
+function serializeExtension(extension) {
+ let obj = {};
+ Lang.copyProperties(extension.metadata, obj);
+
+ SERIALIZED_PROPERTIES.forEach(prop => {
+ obj[prop] = extension[prop];
+ });
+
+ let res = {};
+ for (let key in obj) {
+ let val = obj[key];
+ let type;
+ switch (typeof val) {
+ case 'string':
+ type = 's';
+ break;
+ case 'number':
+ type = 'd';
+ break;
+ case 'boolean':
+ type = 'b';
+ break;
+ default:
+ continue;
+ }
+ res[key] = GLib.Variant.new(type, val);
+ }
+
+ return res;
+}
+
+function deserializeExtension(variant) {
+ let res = { metadata: {} };
+ for (let prop in variant) {
+ let val = variant[prop].unpack();
+ if (SERIALIZED_PROPERTIES.includes(prop))
+ res[prop] = val;
+ else
+ res.metadata[prop] = val;
+ }
+ // add the 2 additional properties to create a valid extension object, as createExtensionObject()
+ res.uuid = res.metadata.uuid;
+ res.dir = Gio.File.new_for_path(res.path);
+ return res;
+}
+
+function installImporter(extension) {
+ let oldSearchPath = imports.searchPath.slice(); // make a copy
+ imports.searchPath = [extension.dir.get_parent().get_path()];
+ // importing a "subdir" creates a new importer object that doesn't affect
+ // the global one
+ extension.imports = imports[extension.uuid];
+ imports.searchPath = oldSearchPath;
+}
diff --git a/js/misc/fileUtils.js b/js/misc/fileUtils.js
new file mode 100644
index 0000000..2f1798b
--- /dev/null
+++ b/js/misc/fileUtils.js
@@ -0,0 +1,117 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported collectFromDatadirs, recursivelyDeleteDir,
+ recursivelyMoveDir, loadInterfaceXML, loadSubInterfaceXML */
+
+const { Gio, GLib } = imports.gi;
+const Config = imports.misc.config;
+
+function collectFromDatadirs(subdir, includeUserDir, processFile) {
+ let dataDirs = GLib.get_system_data_dirs();
+ if (includeUserDir)
+ dataDirs.unshift(GLib.get_user_data_dir());
+
+ for (let i = 0; i < dataDirs.length; i++) {
+ let path = GLib.build_filenamev([dataDirs[i], 'gnome-shell', subdir]);
+ let dir = Gio.File.new_for_path(path);
+
+ let fileEnum;
+ try {
+ fileEnum = dir.enumerate_children('standard::name,standard::type',
+ Gio.FileQueryInfoFlags.NONE, null);
+ } catch (e) {
+ fileEnum = null;
+ }
+ if (fileEnum != null) {
+ let info;
+ while ((info = fileEnum.next_file(null)))
+ processFile(fileEnum.get_child(info), info);
+ }
+ }
+}
+
+function recursivelyDeleteDir(dir, deleteParent) {
+ let children = dir.enumerate_children('standard::name,standard::type',
+ Gio.FileQueryInfoFlags.NONE, null);
+
+ let info;
+ while ((info = children.next_file(null)) != null) {
+ let type = info.get_file_type();
+ let child = dir.get_child(info.get_name());
+ if (type == Gio.FileType.REGULAR)
+ child.delete(null);
+ else if (type == Gio.FileType.DIRECTORY)
+ recursivelyDeleteDir(child, true);
+ }
+
+ if (deleteParent)
+ dir.delete(null);
+}
+
+function recursivelyMoveDir(srcDir, destDir) {
+ let children = srcDir.enumerate_children('standard::name,standard::type',
+ Gio.FileQueryInfoFlags.NONE, null);
+
+ if (!destDir.query_exists(null))
+ destDir.make_directory_with_parents(null);
+
+ let info;
+ while ((info = children.next_file(null)) != null) {
+ let type = info.get_file_type();
+ let srcChild = srcDir.get_child(info.get_name());
+ let destChild = destDir.get_child(info.get_name());
+ if (type == Gio.FileType.REGULAR)
+ srcChild.move(destChild, Gio.FileCopyFlags.NONE, null, null);
+ else if (type == Gio.FileType.DIRECTORY)
+ recursivelyMoveDir(srcChild, destChild);
+ }
+}
+
+let _ifaceResource = null;
+function ensureIfaceResource() {
+ if (_ifaceResource)
+ return;
+
+ // don't use global.datadir so the method is usable from tests/tools
+ let dir = GLib.getenv('GNOME_SHELL_DATADIR') || Config.PKGDATADIR;
+ let path = `${dir}/gnome-shell-dbus-interfaces.gresource`;
+ _ifaceResource = Gio.Resource.load(path);
+ _ifaceResource._register();
+}
+
+function loadInterfaceXML(iface) {
+ ensureIfaceResource();
+
+ let uri = `resource:///org/gnome/shell/dbus-interfaces/${iface}.xml`;
+ let f = Gio.File.new_for_uri(uri);
+
+ try {
+ let [ok_, bytes] = f.load_contents(null);
+ return imports.byteArray.toString(bytes);
+ } catch (e) {
+ log(`Failed to load D-Bus interface ${iface}`);
+ }
+
+ return null;
+}
+
+function loadSubInterfaceXML(iface, ifaceFile) {
+ let xml = loadInterfaceXML(ifaceFile);
+ if (!xml)
+ return null;
+
+ let ifaceStartTag = `<interface name="${iface}">`;
+ let ifaceStopTag = '</interface>';
+ let ifaceStartIndex = xml.indexOf(ifaceStartTag);
+ let ifaceEndIndex = xml.indexOf(ifaceStopTag, ifaceStartIndex + 1) + ifaceStopTag.length;
+
+ let xmlHeader = '<!DOCTYPE node PUBLIC\n' +
+ '\'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\'\n' +
+ '\'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\'>\n' +
+ '<node>\n';
+ let xmlFooter = '</node>';
+
+ return (
+ xmlHeader +
+ xml.substr(ifaceStartIndex, ifaceEndIndex - ifaceStartIndex) +
+ xmlFooter);
+}
diff --git a/js/misc/gnomeSession.js b/js/misc/gnomeSession.js
new file mode 100644
index 0000000..2df9c84
--- /dev/null
+++ b/js/misc/gnomeSession.js
@@ -0,0 +1,37 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported PresenceStatus, Presence, Inhibitor, SessionManager */
+
+const Gio = imports.gi.Gio;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const PresenceIface = loadInterfaceXML('org.gnome.SessionManager.Presence');
+
+var PresenceStatus = {
+ AVAILABLE: 0,
+ INVISIBLE: 1,
+ BUSY: 2,
+ IDLE: 3,
+};
+
+var PresenceProxy = Gio.DBusProxy.makeProxyWrapper(PresenceIface);
+function Presence(initCallback, cancellable) {
+ return new PresenceProxy(Gio.DBus.session, 'org.gnome.SessionManager',
+ '/org/gnome/SessionManager/Presence', initCallback, cancellable);
+}
+
+// Note inhibitors are immutable objects, so they don't
+// change at runtime (changes always come in the form
+// of new inhibitors)
+const InhibitorIface = loadInterfaceXML('org.gnome.SessionManager.Inhibitor');
+var InhibitorProxy = Gio.DBusProxy.makeProxyWrapper(InhibitorIface);
+function Inhibitor(objectPath, initCallback, cancellable) {
+ return new InhibitorProxy(Gio.DBus.session, 'org.gnome.SessionManager', objectPath, initCallback, cancellable);
+}
+
+// Not the full interface, only the methods we use
+const SessionManagerIface = loadInterfaceXML('org.gnome.SessionManager');
+var SessionManagerProxy = Gio.DBusProxy.makeProxyWrapper(SessionManagerIface);
+function SessionManager(initCallback, cancellable) {
+ return new SessionManagerProxy(Gio.DBus.session, 'org.gnome.SessionManager', '/org/gnome/SessionManager', initCallback, cancellable);
+}
diff --git a/js/misc/history.js b/js/misc/history.js
new file mode 100644
index 0000000..c82722c
--- /dev/null
+++ b/js/misc/history.js
@@ -0,0 +1,109 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Signals = imports.signals;
+const Clutter = imports.gi.Clutter;
+const Params = imports.misc.params;
+
+var DEFAULT_LIMIT = 512;
+
+var HistoryManager = class {
+ constructor(params) {
+ params = Params.parse(params, { gsettingsKey: null,
+ limit: DEFAULT_LIMIT,
+ entry: null });
+
+ this._key = params.gsettingsKey;
+ this._limit = params.limit;
+
+ this._historyIndex = 0;
+ if (this._key) {
+ this._history = global.settings.get_strv(this._key);
+ global.settings.connect(`changed::${this._key}`,
+ this._historyChanged.bind(this));
+
+ } else {
+ this._history = [];
+ }
+
+ this._entry = params.entry;
+
+ if (this._entry) {
+ this._entry.connect('key-press-event',
+ this._onEntryKeyPress.bind(this));
+ }
+ }
+
+ _historyChanged() {
+ this._history = global.settings.get_strv(this._key);
+ this._historyIndex = this._history.length;
+ }
+
+ _setPrevItem(text) {
+ if (this._historyIndex <= 0)
+ return false;
+
+ if (text)
+ this._history[this._historyIndex] = text;
+ this._historyIndex--;
+ this._indexChanged();
+ return true;
+ }
+
+ _setNextItem(text) {
+ if (this._historyIndex >= this._history.length)
+ return false;
+
+ if (text)
+ this._history[this._historyIndex] = text;
+ this._historyIndex++;
+ this._indexChanged();
+ return true;
+ }
+
+ lastItem() {
+ if (this._historyIndex != this._history.length) {
+ this._historyIndex = this._history.length;
+ this._indexChanged();
+ }
+
+ return this._historyIndex ? this._history[this._historyIndex - 1] : null;
+ }
+
+ addItem(input) {
+ if (this._history.length == 0 ||
+ this._history[this._history.length - 1] != input) {
+
+ this._history = this._history.filter(entry => entry != input);
+ this._history.push(input);
+ this._save();
+ }
+ this._historyIndex = this._history.length;
+ }
+
+ _onEntryKeyPress(entry, event) {
+ let symbol = event.get_key_symbol();
+ if (symbol == Clutter.KEY_Up)
+ return this._setPrevItem(entry.get_text());
+ else if (symbol == Clutter.KEY_Down)
+ return this._setNextItem(entry.get_text());
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ _indexChanged() {
+ let current = this._history[this._historyIndex] || '';
+ this.emit('changed', current);
+
+ if (this._entry)
+ this._entry.set_text(current);
+ }
+
+ _save() {
+ if (this._history.length > this._limit)
+ this._history.splice(0, this._history.length - this._limit);
+
+ if (this._key)
+ global.settings.set_strv(this._key, this._history);
+ }
+};
+Signals.addSignalMethods(HistoryManager.prototype);
diff --git a/js/misc/ibusManager.js b/js/misc/ibusManager.js
new file mode 100644
index 0000000..d9a9a9d
--- /dev/null
+++ b/js/misc/ibusManager.js
@@ -0,0 +1,276 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported getIBusManager */
+
+const { Gio, GLib, IBus, Meta } = imports.gi;
+const Signals = imports.signals;
+
+const IBusCandidatePopup = imports.ui.ibusCandidatePopup;
+
+Gio._promisify(IBus.Bus.prototype,
+ 'list_engines_async', 'list_engines_async_finish');
+Gio._promisify(IBus.Bus.prototype,
+ 'request_name_async', 'request_name_async_finish');
+Gio._promisify(IBus.Bus.prototype,
+ 'get_global_engine_async', 'get_global_engine_async_finish');
+Gio._promisify(IBus.Bus.prototype,
+ 'set_global_engine_async', 'set_global_engine_async_finish');
+
+// Ensure runtime version matches
+_checkIBusVersion(1, 5, 2);
+
+let _ibusManager = null;
+
+function _checkIBusVersion(requiredMajor, requiredMinor, requiredMicro) {
+ if ((IBus.MAJOR_VERSION > requiredMajor) ||
+ (IBus.MAJOR_VERSION == requiredMajor && IBus.MINOR_VERSION > requiredMinor) ||
+ (IBus.MAJOR_VERSION == requiredMajor && IBus.MINOR_VERSION == requiredMinor &&
+ IBus.MICRO_VERSION >= requiredMicro))
+ return;
+
+ throw "Found IBus version %d.%d.%d but required is %d.%d.%d"
+ .format(IBus.MAJOR_VERSION, IBus.MINOR_VERSION, IBus.MINOR_VERSION,
+ requiredMajor, requiredMinor, requiredMicro);
+}
+
+function getIBusManager() {
+ if (_ibusManager == null)
+ _ibusManager = new IBusManager();
+ return _ibusManager;
+}
+
+var IBusManager = class {
+ constructor() {
+ IBus.init();
+
+ // This is the longest we'll keep the keyboard frozen until an input
+ // source is active.
+ this._MAX_INPUT_SOURCE_ACTIVATION_TIME = 4000; // ms
+ this._PRELOAD_ENGINES_DELAY_TIME = 30; // sec
+
+
+ this._candidatePopup = new IBusCandidatePopup.CandidatePopup();
+
+ this._panelService = null;
+ this._engines = new Map();
+ this._ready = false;
+ this._registerPropertiesId = 0;
+ this._currentEngineName = null;
+ this._preloadEnginesId = 0;
+
+ this._ibus = IBus.Bus.new_async();
+ this._ibus.connect('connected', this._onConnected.bind(this));
+ this._ibus.connect('disconnected', this._clear.bind(this));
+ // Need to set this to get 'global-engine-changed' emitions
+ this._ibus.set_watch_ibus_signal(true);
+ this._ibus.connect('global-engine-changed', this._engineChanged.bind(this));
+
+ this._spawn(Meta.is_wayland_compositor() ? [] : ['--xim']);
+ }
+
+ _spawn(extraArgs = []) {
+ try {
+ let cmdLine = ['ibus-daemon', '--panel', 'disable', ...extraArgs];
+ let launcher = Gio.SubprocessLauncher.new(Gio.SubprocessFlags.NONE);
+ // Forward the right X11 Display for ibus-x11
+ let display = GLib.getenv('GNOME_SETUP_DISPLAY');
+ if (display)
+ launcher.setenv('DISPLAY', display, true);
+ launcher.spawnv(cmdLine);
+ } catch (e) {
+ log(`Failed to launch ibus-daemon: ${e.message}`);
+ }
+ }
+
+ restartDaemon(extraArgs = []) {
+ this._spawn(['-r', ...extraArgs]);
+ }
+
+ _clear() {
+ if (this._cancellable) {
+ this._cancellable.cancel();
+ this._cancellable = null;
+ }
+
+ if (this._preloadEnginesId) {
+ GLib.source_remove(this._preloadEnginesId);
+ this._preloadEnginesId = 0;
+ }
+
+ if (this._panelService)
+ this._panelService.destroy();
+
+ this._panelService = null;
+ this._candidatePopup.setPanelService(null);
+ this._engines.clear();
+ this._ready = false;
+ this._registerPropertiesId = 0;
+ this._currentEngineName = null;
+
+ this.emit('ready', false);
+ }
+
+ _onConnected() {
+ this._cancellable = new Gio.Cancellable();
+ this._initEngines();
+ this._initPanelService();
+ }
+
+ async _initEngines() {
+ try {
+ const enginesList =
+ await this._ibus.list_engines_async(-1, this._cancellable);
+ for (let i = 0; i < enginesList.length; ++i) {
+ let name = enginesList[i].get_name();
+ this._engines.set(name, enginesList[i]);
+ }
+ this._updateReadiness();
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ return;
+
+ logError(e);
+ this._clear();
+ }
+ }
+
+ async _initPanelService() {
+ try {
+ await this._ibus.request_name_async(IBus.SERVICE_PANEL,
+ IBus.BusNameFlag.REPLACE_EXISTING, -1, this._cancellable);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ logError(e);
+ this._clear();
+ }
+ return;
+ }
+
+ this._panelService = new IBus.PanelService({
+ connection: this._ibus.get_connection(),
+ object_path: IBus.PATH_PANEL,
+ });
+ this._candidatePopup.setPanelService(this._panelService);
+ this._panelService.connect('update-property', this._updateProperty.bind(this));
+ this._panelService.connect('set-cursor-location', (ps, x, y, w, h) => {
+ let cursorLocation = { x, y, width: w, height: h };
+ this.emit('set-cursor-location', cursorLocation);
+ });
+ this._panelService.connect('focus-in', (panel, path) => {
+ if (!GLib.str_has_suffix(path, '/InputContext_1'))
+ this.emit('focus-in');
+ });
+ this._panelService.connect('focus-out', () => this.emit('focus-out'));
+
+ try {
+ // IBus versions older than 1.5.10 have a bug which
+ // causes spurious set-content-type emissions when
+ // switching input focus that temporarily lose purpose
+ // and hints defeating its intended semantics and
+ // confusing users. We thus don't use it in that case.
+ _checkIBusVersion(1, 5, 10);
+ this._panelService.connect('set-content-type', this._setContentType.bind(this));
+ } catch (e) {
+ }
+ this._updateReadiness();
+
+ try {
+ // If an engine is already active we need to get its properties
+ const engine =
+ await this._ibus.get_global_engine_async(-1, this._cancellable);
+ this._engineChanged(this._ibus, engine.get_name());
+ } catch (e) {
+ }
+ }
+
+ _updateReadiness() {
+ this._ready = this._engines.size > 0 && this._panelService != null;
+ this.emit('ready', this._ready);
+ }
+
+ _engineChanged(bus, engineName) {
+ if (!this._ready)
+ return;
+
+ this._currentEngineName = engineName;
+
+ if (this._registerPropertiesId != 0)
+ return;
+
+ this._registerPropertiesId =
+ this._panelService.connect('register-properties', (p, props) => {
+ if (!props.get(0))
+ return;
+
+ this._panelService.disconnect(this._registerPropertiesId);
+ this._registerPropertiesId = 0;
+
+ this.emit('properties-registered', this._currentEngineName, props);
+ });
+ }
+
+ _updateProperty(panel, prop) {
+ this.emit('property-updated', this._currentEngineName, prop);
+ }
+
+ _setContentType(panel, purpose, hints) {
+ this.emit('set-content-type', purpose, hints);
+ }
+
+ activateProperty(key, state) {
+ this._panelService.property_activate(key, state);
+ }
+
+ getEngineDesc(id) {
+ if (!this._ready || !this._engines.has(id))
+ return null;
+
+ return this._engines.get(id);
+ }
+
+ async setEngine(id, callback) {
+ // Send id even if id == this._currentEngineName
+ // because 'properties-registered' signal can be emitted
+ // while this._ibusSources == null on a lock screen.
+ if (!this._ready) {
+ if (callback)
+ callback();
+ return;
+ }
+
+ try {
+ await this._ibus.set_global_engine_async(id,
+ this._MAX_INPUT_SOURCE_ACTIVATION_TIME,
+ this._cancellable);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ }
+ if (callback)
+ callback();
+ }
+
+ preloadEngines(ids) {
+ if (!this._ibus || ids.length == 0)
+ return;
+
+ if (this._preloadEnginesId != 0) {
+ GLib.source_remove(this._preloadEnginesId);
+ this._preloadEnginesId = 0;
+ }
+
+ this._preloadEnginesId =
+ GLib.timeout_add_seconds(
+ GLib.PRIORITY_DEFAULT,
+ this._PRELOAD_ENGINES_DELAY_TIME,
+ () => {
+ this._ibus.preload_engines_async(
+ ids,
+ -1,
+ this._cancellable,
+ null);
+ this._preloadEnginesId = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+};
+Signals.addSignalMethods(IBusManager.prototype);
diff --git a/js/misc/inputMethod.js b/js/misc/inputMethod.js
new file mode 100644
index 0000000..0bc1462
--- /dev/null
+++ b/js/misc/inputMethod.js
@@ -0,0 +1,285 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported InputMethod */
+const { Clutter, GLib, Gio, GObject, IBus } = imports.gi;
+
+const Keyboard = imports.ui.status.keyboard;
+
+Gio._promisify(IBus.Bus.prototype,
+ 'create_input_context_async', 'create_input_context_async_finish');
+
+var HIDE_PANEL_TIME = 50;
+
+var InputMethod = GObject.registerClass(
+class InputMethod extends Clutter.InputMethod {
+ _init() {
+ super._init();
+ this._hints = 0;
+ this._purpose = 0;
+ this._currentFocus = null;
+ this._preeditStr = '';
+ this._preeditPos = 0;
+ this._preeditVisible = false;
+ this._hidePanelId = 0;
+ this._ibus = IBus.Bus.new_async();
+ this._ibus.connect('connected', this._onConnected.bind(this));
+ this._ibus.connect('disconnected', this._clear.bind(this));
+ this.connect('notify::can-show-preedit', this._updateCapabilities.bind(this));
+
+ this._inputSourceManager = Keyboard.getInputSourceManager();
+ this._sourceChangedId = this._inputSourceManager.connect('current-source-changed',
+ this._onSourceChanged.bind(this));
+ this._currentSource = this._inputSourceManager.currentSource;
+
+ if (this._ibus.is_connected())
+ this._onConnected();
+ }
+
+ get currentFocus() {
+ return this._currentFocus;
+ }
+
+ _updateCapabilities() {
+ let caps = IBus.Capabilite.PREEDIT_TEXT | IBus.Capabilite.FOCUS | IBus.Capabilite.SURROUNDING_TEXT;
+
+ if (this._context)
+ this._context.set_capabilities(caps);
+ }
+
+ _onSourceChanged() {
+ this._currentSource = this._inputSourceManager.currentSource;
+ }
+
+ async _onConnected() {
+ this._cancellable = new Gio.Cancellable();
+ try {
+ this._context = await this._ibus.create_input_context_async(
+ 'gnome-shell', -1, this._cancellable);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ logError(e);
+ this._clear();
+ }
+ return;
+ }
+
+ this._context.connect('commit-text', this._onCommitText.bind(this));
+ this._context.connect('delete-surrounding-text', this._onDeleteSurroundingText.bind(this));
+ this._context.connect('update-preedit-text', this._onUpdatePreeditText.bind(this));
+ this._context.connect('show-preedit-text', this._onShowPreeditText.bind(this));
+ this._context.connect('hide-preedit-text', this._onHidePreeditText.bind(this));
+ this._context.connect('forward-key-event', this._onForwardKeyEvent.bind(this));
+ this._context.connect('destroy', this._clear.bind(this));
+
+ this._updateCapabilities();
+ }
+
+ _clear() {
+ if (this._cancellable) {
+ this._cancellable.cancel();
+ this._cancellable = null;
+ }
+
+ this._context = null;
+ this._hints = 0;
+ this._purpose = 0;
+ this._preeditStr = '';
+ this._preeditPos = 0;
+ this._preeditVisible = false;
+ }
+
+ _emitRequestSurrounding() {
+ if (this._context.needs_surrounding_text())
+ this.emit('request-surrounding');
+ }
+
+ _onCommitText(_context, text) {
+ this.commit(text.get_text());
+ }
+
+ _onDeleteSurroundingText(_context, offset, nchars) {
+ try {
+ this.delete_surrounding(offset, nchars);
+ } catch (e) {
+ // We may get out of bounds for negative offset on older mutter
+ this.delete_surrounding(0, nchars + offset);
+ }
+ }
+
+ _onUpdatePreeditText(_context, text, pos, visible) {
+ if (text == null)
+ return;
+
+ let preedit = text.get_text();
+
+ if (visible)
+ this.set_preedit_text(preedit, pos);
+ else if (this._preeditVisible)
+ this.set_preedit_text(null, pos);
+
+ this._preeditStr = preedit;
+ this._preeditPos = pos;
+ this._preeditVisible = visible;
+ }
+
+ _onShowPreeditText() {
+ this._preeditVisible = true;
+ this.set_preedit_text(this._preeditStr, this._preeditPos);
+ }
+
+ _onHidePreeditText() {
+ this.set_preedit_text(null, this._preeditPos);
+ this._preeditVisible = false;
+ }
+
+ _onForwardKeyEvent(_context, keyval, keycode, state) {
+ let press = (state & IBus.ModifierType.RELEASE_MASK) == 0;
+ state &= ~IBus.ModifierType.RELEASE_MASK;
+
+ let curEvent = Clutter.get_current_event();
+ let time;
+ if (curEvent)
+ time = curEvent.get_time();
+ else
+ time = global.display.get_current_time_roundtrip();
+
+ this.forward_key(keyval, keycode + 8, state & Clutter.ModifierType.MODIFIER_MASK, time, press);
+ }
+
+ vfunc_focus_in(focus) {
+ this._currentFocus = focus;
+ if (this._context) {
+ this._context.focus_in();
+ this._emitRequestSurrounding();
+ }
+
+ if (this._hidePanelId) {
+ GLib.source_remove(this._hidePanelId);
+ this._hidePanelId = 0;
+ }
+ }
+
+ vfunc_focus_out() {
+ this._currentFocus = null;
+ if (this._context)
+ this._context.focus_out();
+
+ if (this._preeditStr) {
+ // Unset any preedit text
+ this.set_preedit_text(null, 0);
+ this._preeditStr = null;
+ }
+
+ this._hidePanelId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, HIDE_PANEL_TIME, () => {
+ this.set_input_panel_state(Clutter.InputPanelState.OFF);
+ this._hidePanelId = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ vfunc_reset() {
+ if (this._context) {
+ this._context.reset();
+ this._emitRequestSurrounding();
+ }
+
+ if (this._preeditStr) {
+ // Unset any preedit text
+ this.set_preedit_text(null, 0);
+ this._preeditStr = null;
+ }
+ }
+
+ vfunc_set_cursor_location(rect) {
+ if (this._context) {
+ this._context.set_cursor_location(rect.get_x(), rect.get_y(),
+ rect.get_width(), rect.get_height());
+ this._emitRequestSurrounding();
+ }
+ }
+
+ vfunc_set_surrounding(text, cursor, anchor) {
+ if (!this._context || !text)
+ return;
+
+ let ibusText = IBus.Text.new_from_string(text);
+ this._context.set_surrounding_text(ibusText, cursor, anchor);
+ }
+
+ vfunc_update_content_hints(hints) {
+ let ibusHints = 0;
+ if (hints & Clutter.InputContentHintFlags.COMPLETION)
+ ibusHints |= IBus.InputHints.WORD_COMPLETION;
+ if (hints & Clutter.InputContentHintFlags.SPELLCHECK)
+ ibusHints |= IBus.InputHints.SPELLCHECK;
+ if (hints & Clutter.InputContentHintFlags.AUTO_CAPITALIZATION)
+ ibusHints |= IBus.InputHints.UPPERCASE_SENTENCES;
+ if (hints & Clutter.InputContentHintFlags.LOWERCASE)
+ ibusHints |= IBus.InputHints.LOWERCASE;
+ if (hints & Clutter.InputContentHintFlags.UPPERCASE)
+ ibusHints |= IBus.InputHints.UPPERCASE_CHARS;
+ if (hints & Clutter.InputContentHintFlags.TITLECASE)
+ ibusHints |= IBus.InputHints.UPPERCASE_WORDS;
+
+ this._hints = ibusHints;
+ if (this._context)
+ this._context.set_content_type(this._purpose, this._hints);
+ }
+
+ vfunc_update_content_purpose(purpose) {
+ let ibusPurpose = 0;
+ if (purpose == Clutter.InputContentPurpose.NORMAL)
+ ibusPurpose = IBus.InputPurpose.FREE_FORM;
+ else if (purpose == Clutter.InputContentPurpose.ALPHA)
+ ibusPurpose = IBus.InputPurpose.ALPHA;
+ else if (purpose == Clutter.InputContentPurpose.DIGITS)
+ ibusPurpose = IBus.InputPurpose.DIGITS;
+ else if (purpose == Clutter.InputContentPurpose.NUMBER)
+ ibusPurpose = IBus.InputPurpose.NUMBER;
+ else if (purpose == Clutter.InputContentPurpose.PHONE)
+ ibusPurpose = IBus.InputPurpose.PHONE;
+ else if (purpose == Clutter.InputContentPurpose.URL)
+ ibusPurpose = IBus.InputPurpose.URL;
+ else if (purpose == Clutter.InputContentPurpose.EMAIL)
+ ibusPurpose = IBus.InputPurpose.EMAIL;
+ else if (purpose == Clutter.InputContentPurpose.NAME)
+ ibusPurpose = IBus.InputPurpose.NAME;
+ else if (purpose == Clutter.InputContentPurpose.PASSWORD)
+ ibusPurpose = IBus.InputPurpose.PASSWORD;
+
+ this._purpose = ibusPurpose;
+ if (this._context)
+ this._context.set_content_type(this._purpose, this._hints);
+ }
+
+ vfunc_filter_key_event(event) {
+ if (!this._context)
+ return false;
+ if (!this._currentSource)
+ return false;
+
+ let state = event.get_state();
+ if (state & IBus.ModifierType.IGNORED_MASK)
+ return false;
+
+ if (event.type() == Clutter.EventType.KEY_RELEASE)
+ state |= IBus.ModifierType.RELEASE_MASK;
+
+ this._context.process_key_event_async(
+ event.get_key_symbol(),
+ event.get_key_code() - 8, // Convert XKB keycodes to evcodes
+ state, -1, this._cancellable,
+ (context, res) => {
+ if (context != this._context)
+ return;
+
+ try {
+ let retval = context.process_key_event_async_finish(res);
+ this.notify_key_event(event, retval);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ log(`Error processing key on IM: ${e.message}`);
+ }
+ });
+ return true;
+ }
+});
diff --git a/js/misc/introspect.js b/js/misc/introspect.js
new file mode 100644
index 0000000..bae7f63
--- /dev/null
+++ b/js/misc/introspect.js
@@ -0,0 +1,242 @@
+/* exported IntrospectService */
+const { Gio, GLib, Meta, Shell, St } = imports.gi;
+
+const INTROSPECT_SCHEMA = 'org.gnome.shell';
+const INTROSPECT_KEY = 'introspect';
+const APP_ALLOWLIST = ['org.freedesktop.impl.portal.desktop.gtk'];
+
+const INTROSPECT_DBUS_API_VERSION = 3;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const IntrospectDBusIface = loadInterfaceXML('org.gnome.Shell.Introspect');
+
+var IntrospectService = class {
+ constructor() {
+ this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(IntrospectDBusIface,
+ this);
+ this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/Introspect');
+ Gio.DBus.session.own_name('org.gnome.Shell.Introspect',
+ Gio.BusNameOwnerFlags.REPLACE,
+ null, null);
+
+ this._runningApplications = {};
+ this._runningApplicationsDirty = true;
+ this._activeApplication = null;
+ this._activeApplicationDirty = true;
+ this._animationsEnabled = true;
+
+ this._appSystem = Shell.AppSystem.get_default();
+ this._appSystem.connect('app-state-changed',
+ () => {
+ this._runningApplicationsDirty = true;
+ this._syncRunningApplications();
+ });
+
+ this._introspectSettings = new Gio.Settings({
+ schema_id: INTROSPECT_SCHEMA,
+ });
+
+ let tracker = Shell.WindowTracker.get_default();
+ tracker.connect('notify::focus-app',
+ () => {
+ this._activeApplicationDirty = true;
+ this._syncRunningApplications();
+ });
+
+ this._syncRunningApplications();
+
+ this._allowlistMap = new Map();
+ APP_ALLOWLIST.forEach(appName => {
+ Gio.DBus.watch_name(Gio.BusType.SESSION,
+ appName,
+ Gio.BusNameWatcherFlags.NONE,
+ (conn, name, owner) => this._allowlistMap.set(name, owner),
+ (conn, name) => this._allowlistMap.delete(name));
+ });
+
+ this._settings = St.Settings.get();
+ this._settings.connect('notify::enable-animations',
+ this._syncAnimationsEnabled.bind(this));
+ this._syncAnimationsEnabled();
+
+ const monitorManager = Meta.MonitorManager.get();
+ monitorManager.connect('monitors-changed',
+ this._syncScreenSize.bind(this));
+ this._syncScreenSize();
+ }
+
+ _isStandaloneApp(app) {
+ return app.get_windows().some(w => w.transient_for == null);
+ }
+
+ _isIntrospectEnabled() {
+ return this._introspectSettings.get_boolean(INTROSPECT_KEY);
+ }
+
+ _isSenderAllowed(sender) {
+ return [...this._allowlistMap.values()].includes(sender);
+ }
+
+ _getSandboxedAppId(app) {
+ let ids = app.get_windows().map(w => w.get_sandboxed_app_id());
+ return ids.find(id => id != null);
+ }
+
+ _syncRunningApplications() {
+ let tracker = Shell.WindowTracker.get_default();
+ let apps = this._appSystem.get_running();
+ let seatName = "seat0";
+ let newRunningApplications = {};
+
+ let newActiveApplication = null;
+ let focusedApp = tracker.focus_app;
+
+ for (let app of apps) {
+ let appInfo = {};
+ let isAppActive = focusedApp == app;
+
+ if (!this._isStandaloneApp(app))
+ continue;
+
+ if (isAppActive) {
+ appInfo['active-on-seats'] = new GLib.Variant('as', [seatName]);
+ newActiveApplication = app.get_id();
+ }
+
+ let sandboxedAppId = this._getSandboxedAppId(app);
+ if (sandboxedAppId)
+ appInfo['sandboxed-app-id'] = new GLib.Variant('s', sandboxedAppId);
+
+ newRunningApplications[app.get_id()] = appInfo;
+ }
+
+ if (this._runningApplicationsDirty ||
+ (this._activeApplicationDirty &&
+ this._activeApplication != newActiveApplication)) {
+ this._runningApplications = newRunningApplications;
+ this._activeApplication = newActiveApplication;
+
+ this._dbusImpl.emit_signal('RunningApplicationsChanged', null);
+ }
+ this._runningApplicationsDirty = false;
+ this._activeApplicationDirty = false;
+ }
+
+ _isEligibleWindow(window) {
+ if (window.is_override_redirect())
+ return false;
+
+ let type = window.get_window_type();
+ return type == Meta.WindowType.NORMAL ||
+ type == Meta.WindowType.DIALOG ||
+ type == Meta.WindowType.MODAL_DIALOG ||
+ type == Meta.WindowType.UTILITY;
+ }
+
+ _isInvocationAllowed(invocation) {
+ if (this._isIntrospectEnabled())
+ return true;
+
+ if (this._isSenderAllowed(invocation.get_sender()))
+ return true;
+
+ return false;
+ }
+
+ GetRunningApplicationsAsync(params, invocation) {
+ if (!this._isInvocationAllowed(invocation)) {
+ invocation.return_error_literal(Gio.DBusError,
+ Gio.DBusError.ACCESS_DENIED,
+ 'App introspection not allowed');
+ return;
+ }
+
+ invocation.return_value(new GLib.Variant('(a{sa{sv}})', [this._runningApplications]));
+ }
+
+ GetWindowsAsync(params, invocation) {
+ let focusWindow = global.display.get_focus_window();
+ let apps = this._appSystem.get_running();
+ let windowsList = {};
+
+ if (!this._isInvocationAllowed(invocation)) {
+ invocation.return_error_literal(Gio.DBusError,
+ Gio.DBusError.ACCESS_DENIED,
+ 'App introspection not allowed');
+ return;
+ }
+
+ for (let app of apps) {
+ let windows = app.get_windows();
+ for (let window of windows) {
+
+ if (!this._isEligibleWindow(window))
+ continue;
+
+ let windowId = window.get_id();
+ let frameRect = window.get_frame_rect();
+ let title = window.get_title();
+ let wmClass = window.get_wm_class();
+ let sandboxedAppId = window.get_sandboxed_app_id();
+
+ windowsList[windowId] = {
+ 'app-id': GLib.Variant.new('s', app.get_id()),
+ 'client-type': GLib.Variant.new('u', window.get_client_type()),
+ 'is-hidden': GLib.Variant.new('b', window.is_hidden()),
+ 'has-focus': GLib.Variant.new('b', window == focusWindow),
+ 'width': GLib.Variant.new('u', frameRect.width),
+ 'height': GLib.Variant.new('u', frameRect.height),
+ };
+
+ // These properties may not be available for all windows:
+ if (title != null)
+ windowsList[windowId]['title'] = GLib.Variant.new('s', title);
+
+ if (wmClass != null)
+ windowsList[windowId]['wm-class'] = GLib.Variant.new('s', wmClass);
+
+ if (sandboxedAppId != null) {
+ windowsList[windowId]['sandboxed-app-id'] =
+ GLib.Variant.new('s', sandboxedAppId);
+ }
+ }
+ }
+ invocation.return_value(new GLib.Variant('(a{ta{sv}})', [windowsList]));
+ }
+
+ _syncAnimationsEnabled() {
+ let wasAnimationsEnabled = this._animationsEnabled;
+ this._animationsEnabled = this._settings.enable_animations;
+ if (wasAnimationsEnabled !== this._animationsEnabled) {
+ let variant = new GLib.Variant('b', this._animationsEnabled);
+ this._dbusImpl.emit_property_changed('AnimationsEnabled', variant);
+ }
+ }
+
+ _syncScreenSize() {
+ const oldScreenWidth = this._screenWidth;
+ const oldScreenHeight = this._screenHeight;
+ this._screenWidth = global.screen_width;
+ this._screenHeight = global.screen_height;
+
+ if (oldScreenWidth !== this._screenWidth ||
+ oldScreenHeight !== this._screenHeight) {
+ const variant = new GLib.Variant('(ii)',
+ [this._screenWidth, this._screenHeight]);
+ this._dbusImpl.emit_property_changed('ScreenSize', variant);
+ }
+ }
+
+ get AnimationsEnabled() {
+ return this._animationsEnabled;
+ }
+
+ get ScreenSize() {
+ return [this._screenWidth, this._screenHeight];
+ }
+
+ get version() {
+ return INTROSPECT_DBUS_API_VERSION;
+ }
+};
diff --git a/js/misc/jsParse.js b/js/misc/jsParse.js
new file mode 100644
index 0000000..c92465f
--- /dev/null
+++ b/js/misc/jsParse.js
@@ -0,0 +1,237 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* exported getCompletions, getCommonPrefix, getDeclaredConstants */
+
+// Returns a list of potential completions for text. Completions either
+// follow a dot (e.g. foo.ba -> bar) or they are picked from globalCompletionList (e.g. fo -> foo)
+// commandHeader is prefixed on any expression before it is eval'ed. It will most likely
+// consist of global constants that might not carry over from the calling environment.
+//
+// This function is likely the one you want to call from external modules
+function getCompletions(text, commandHeader, globalCompletionList) {
+ let methods = [];
+ let expr_, base;
+ let attrHead = '';
+ if (globalCompletionList == null)
+ globalCompletionList = [];
+
+ let offset = getExpressionOffset(text, text.length - 1);
+ if (offset >= 0) {
+ text = text.slice(offset);
+
+ // Look for expressions like "Main.panel.foo" and match Main.panel and foo
+ let matches = text.match(/(.*)\.(.*)/);
+ if (matches) {
+ [expr_, base, attrHead] = matches;
+
+ methods = getPropertyNamesFromExpression(base, commandHeader).filter(
+ attr => attr.slice(0, attrHead.length) === attrHead);
+ }
+
+ // Look for the empty expression or partially entered words
+ // not proceeded by a dot and match them against global constants
+ matches = text.match(/^(\w*)$/);
+ if (text == '' || matches) {
+ [expr_, attrHead] = matches;
+ methods = globalCompletionList.filter(
+ attr => attr.slice(0, attrHead.length) === attrHead);
+ }
+ }
+
+ return [methods, attrHead];
+}
+
+
+//
+// A few functions for parsing strings of javascript code.
+//
+
+// Identify characters that delimit an expression. That is,
+// if we encounter anything that isn't a letter, '.', ')', or ']',
+// we should stop parsing.
+function isStopChar(c) {
+ return !c.match(/[\w.)\]]/);
+}
+
+// Given the ending position of a quoted string, find where it starts
+function findMatchingQuote(expr, offset) {
+ let quoteChar = expr.charAt(offset);
+ for (let i = offset - 1; i >= 0; --i) {
+ if (expr.charAt(i) == quoteChar && expr.charAt(i - 1) != '\\')
+ return i;
+ }
+ return -1;
+}
+
+// Given the ending position of a regex, find where it starts
+function findMatchingSlash(expr, offset) {
+ for (let i = offset - 1; i >= 0; --i) {
+ if (expr.charAt(i) == '/' && expr.charAt(i - 1) != '\\')
+ return i;
+ }
+ return -1;
+}
+
+// If expr.charAt(offset) is ')' or ']',
+// return the position of the corresponding '(' or '[' bracket.
+// This function does not check for syntactic correctness. e.g.,
+// findMatchingBrace("[(])", 3) returns 1.
+function findMatchingBrace(expr, offset) {
+ let closeBrace = expr.charAt(offset);
+ let openBrace = { ')': '(', ']': '[' }[closeBrace];
+
+ return findTheBrace(expr, offset - 1, openBrace, closeBrace);
+}
+
+function findTheBrace(expr, offset, ...braces) {
+ let [openBrace, closeBrace] = braces;
+
+ if (offset < 0)
+ return -1;
+
+ if (expr.charAt(offset) == openBrace)
+ return offset;
+
+ if (expr.charAt(offset).match(/['"]/))
+ return findTheBrace(expr, findMatchingQuote(expr, offset) - 1, ...braces);
+
+ if (expr.charAt(offset) == '/')
+ return findTheBrace(expr, findMatchingSlash(expr, offset) - 1, ...braces);
+
+ if (expr.charAt(offset) == closeBrace)
+ return findTheBrace(expr, findTheBrace(expr, offset - 1, ...braces) - 1, ...braces);
+
+ return findTheBrace(expr, offset - 1, ...braces);
+}
+
+// Walk expr backwards from offset looking for the beginning of an
+// expression suitable for passing to eval.
+// There is no guarantee of correct javascript syntax between the return
+// value and offset. This function is meant to take a string like
+// "foo(Obj.We.Are.Completing" and allow you to extract "Obj.We.Are.Completing"
+function getExpressionOffset(expr, offset) {
+ while (offset >= 0) {
+ let currChar = expr.charAt(offset);
+
+ if (isStopChar(currChar))
+ return offset + 1;
+
+ if (currChar.match(/[)\]]/))
+ offset = findMatchingBrace(expr, offset);
+
+ --offset;
+ }
+
+ return offset + 1;
+}
+
+// Things with non-word characters or that start with a number
+// are not accessible via .foo notation and so aren't returned
+function isValidPropertyName(w) {
+ return !(w.match(/\W/) || w.match(/^\d/));
+}
+
+// To get all properties (enumerable and not), we need to walk
+// the prototype chain ourselves
+function getAllProps(obj) {
+ if (obj === null || obj === undefined)
+ return [];
+
+ return Object.getOwnPropertyNames(obj).concat(getAllProps(Object.getPrototypeOf(obj)));
+}
+
+// Given a string _expr_, returns all methods
+// that can be accessed via '.' notation.
+// e.g., expr="({ foo: null, bar: null, 4: null })" will
+// return ["foo", "bar", ...] but the list will not include "4",
+// since methods accessed with '.' notation must star with a letter or _.
+function getPropertyNamesFromExpression(expr, commandHeader = '') {
+ let obj = {};
+ if (!isUnsafeExpression(expr)) {
+ try {
+ obj = eval(commandHeader + expr);
+ } catch (e) {
+ return [];
+ }
+ } else {
+ return [];
+ }
+
+ let propsUnique = {};
+ if (typeof obj === 'object') {
+ let allProps = getAllProps(obj);
+ // Get only things we are allowed to complete following a '.'
+ allProps = allProps.filter(isValidPropertyName);
+
+ // Make sure propsUnique contains one key for every
+ // property so we end up with a unique list of properties
+ allProps.map(p => (propsUnique[p] = null));
+ }
+ return Object.keys(propsUnique).sort();
+}
+
+// Given a list of words, returns the longest prefix they all have in common
+function getCommonPrefix(words) {
+ let word = words[0];
+ for (let i = 0; i < word.length; i++) {
+ for (let w = 1; w < words.length; w++) {
+ if (words[w].charAt(i) != word.charAt(i))
+ return word.slice(0, i);
+ }
+ }
+ return word;
+}
+
+// Remove any blocks that are quoted or are in a regex
+function removeLiterals(str) {
+ if (str.length == 0)
+ return '';
+
+ let currChar = str.charAt(str.length - 1);
+ if (currChar == '"' || currChar == '\'') {
+ return removeLiterals(
+ str.slice(0, findMatchingQuote(str, str.length - 1)));
+ } else if (currChar == '/') {
+ return removeLiterals(
+ str.slice(0, findMatchingSlash(str, str.length - 1)));
+ }
+
+ return removeLiterals(str.slice(0, str.length - 1)) + currChar;
+}
+
+// Returns true if there is reason to think that eval(str)
+// will modify the global scope
+function isUnsafeExpression(str) {
+
+ // Check for any sort of assignment
+ // The strategy used is dumb: remove any quotes
+ // or regexs and comparison operators and see if there is an '=' character.
+ // If there is, it might be an unsafe assignment.
+
+ let prunedStr = removeLiterals(str);
+ prunedStr = prunedStr.replace(/[=!]==/g, ''); // replace === and !== with nothing
+ prunedStr = prunedStr.replace(/[=<>!]=/g, ''); // replace ==, <=, >=, != with nothing
+
+ if (prunedStr.match(/[=]/)) {
+ return true;
+ } else if (prunedStr.match(/;/)) {
+ // If we contain a semicolon not inside of a quote/regex, assume we're unsafe as well
+ return true;
+ }
+
+ return false;
+}
+
+// Returns a list of global keywords derived from str
+function getDeclaredConstants(str) {
+ let ret = [];
+ str.split(';').forEach(s => {
+ let base_, keyword;
+ let match = s.match(/const\s+(\w+)\s*=/);
+ if (match) {
+ [base_, keyword] = match;
+ ret.push(keyword);
+ }
+ });
+
+ return ret;
+}
diff --git a/js/misc/keyboardManager.js b/js/misc/keyboardManager.js
new file mode 100644
index 0000000..29fc3b7
--- /dev/null
+++ b/js/misc/keyboardManager.js
@@ -0,0 +1,159 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported getKeyboardManager, holdKeyboard, releaseKeyboard */
+
+const { GLib, GnomeDesktop } = imports.gi;
+
+const Main = imports.ui.main;
+
+var DEFAULT_LOCALE = 'en_US';
+var DEFAULT_LAYOUT = 'us';
+var DEFAULT_VARIANT = '';
+
+let _xkbInfo = null;
+
+function getXkbInfo() {
+ if (_xkbInfo == null)
+ _xkbInfo = new GnomeDesktop.XkbInfo();
+ return _xkbInfo;
+}
+
+let _keyboardManager = null;
+
+function getKeyboardManager() {
+ if (_keyboardManager == null)
+ _keyboardManager = new KeyboardManager();
+ return _keyboardManager;
+}
+
+function releaseKeyboard() {
+ if (Main.modalCount > 0)
+ global.display.unfreeze_keyboard(global.get_current_time());
+ else
+ global.display.ungrab_keyboard(global.get_current_time());
+}
+
+function holdKeyboard() {
+ global.display.freeze_keyboard(global.get_current_time());
+}
+
+var KeyboardManager = class {
+ constructor() {
+ // The XKB protocol doesn't allow for more that 4 layouts in a
+ // keymap. Wayland doesn't impose this limit and libxkbcommon can
+ // handle up to 32 layouts but since we need to support X clients
+ // even as a Wayland compositor, we can't bump this.
+ this.MAX_LAYOUTS_PER_GROUP = 4;
+
+ this._xkbInfo = getXkbInfo();
+ this._current = null;
+ this._localeLayoutInfo = this._getLocaleLayout();
+ this._layoutInfos = {};
+ this._currentKeymap = null;
+ }
+
+ _applyLayoutGroup(group) {
+ let options = this._buildOptionsString();
+ let [layouts, variants] = this._buildGroupStrings(group);
+
+ if (this._currentKeymap &&
+ this._currentKeymap.layouts == layouts &&
+ this._currentKeymap.variants == variants &&
+ this._currentKeymap.options == options)
+ return;
+
+ this._currentKeymap = { layouts, variants, options };
+ global.backend.set_keymap(layouts, variants, options);
+ }
+
+ _applyLayoutGroupIndex(idx) {
+ global.backend.lock_layout_group(idx);
+ }
+
+ apply(id) {
+ let info = this._layoutInfos[id];
+ if (!info)
+ return;
+
+ if (this._current && this._current.group == info.group) {
+ if (this._current.groupIndex != info.groupIndex)
+ this._applyLayoutGroupIndex(info.groupIndex);
+ } else {
+ this._applyLayoutGroup(info.group);
+ this._applyLayoutGroupIndex(info.groupIndex);
+ }
+
+ this._current = info;
+ }
+
+ reapply() {
+ if (!this._current)
+ return;
+
+ this._applyLayoutGroup(this._current.group);
+ this._applyLayoutGroupIndex(this._current.groupIndex);
+ }
+
+ setUserLayouts(ids) {
+ this._current = null;
+ this._layoutInfos = {};
+
+ for (let i = 0; i < ids.length; ++i) {
+ let [found, , , _layout, _variant] = this._xkbInfo.get_layout_info(ids[i]);
+ if (found)
+ this._layoutInfos[ids[i]] = { id: ids[i], layout: _layout, variant: _variant };
+ }
+
+ let i = 0;
+ let group = [];
+ for (let id in this._layoutInfos) {
+ // We need to leave one slot on each group free so that we
+ // can add a layout containing the symbols for the
+ // language used in UI strings to ensure that toolkits can
+ // handle mnemonics like Alt+Š¤ even if the user is
+ // actually typing in a different layout.
+ let groupIndex = i % (this.MAX_LAYOUTS_PER_GROUP - 1);
+ if (groupIndex == 0)
+ group = [];
+
+ let info = this._layoutInfos[id];
+ group[groupIndex] = info;
+ info.group = group;
+ info.groupIndex = groupIndex;
+
+ i += 1;
+ }
+ }
+
+ _getLocaleLayout() {
+ let locale = GLib.get_language_names()[0];
+ if (!locale.includes('_'))
+ locale = DEFAULT_LOCALE;
+
+ let [found, , id] = GnomeDesktop.get_input_source_from_locale(locale);
+ if (!found)
+ [, , id] = GnomeDesktop.get_input_source_from_locale(DEFAULT_LOCALE);
+
+ let _layout, _variant;
+ [found, , , _layout, _variant] = this._xkbInfo.get_layout_info(id);
+ if (found)
+ return { layout: _layout, variant: _variant };
+ else
+ return { layout: DEFAULT_LAYOUT, variant: DEFAULT_VARIANT };
+ }
+
+ _buildGroupStrings(_group) {
+ let group = _group.concat(this._localeLayoutInfo);
+ let layouts = group.map(g => g.layout).join(',');
+ let variants = group.map(g => g.variant).join(',');
+ return [layouts, variants];
+ }
+
+ setKeyboardOptions(options) {
+ this._xkbOptions = options;
+ }
+
+ _buildOptionsString() {
+ let options = this._xkbOptions.join(',');
+ return options;
+ }
+};
diff --git a/js/misc/loginManager.js b/js/misc/loginManager.js
new file mode 100644
index 0000000..55e9289
--- /dev/null
+++ b/js/misc/loginManager.js
@@ -0,0 +1,243 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported canLock, getLoginManager, registerSessionWithGDM */
+
+const { GLib, Gio } = imports.gi;
+const Signals = imports.signals;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const SystemdLoginManagerIface = loadInterfaceXML('org.freedesktop.login1.Manager');
+const SystemdLoginSessionIface = loadInterfaceXML('org.freedesktop.login1.Session');
+const SystemdLoginUserIface = loadInterfaceXML('org.freedesktop.login1.User');
+
+const SystemdLoginManager = Gio.DBusProxy.makeProxyWrapper(SystemdLoginManagerIface);
+const SystemdLoginSession = Gio.DBusProxy.makeProxyWrapper(SystemdLoginSessionIface);
+const SystemdLoginUser = Gio.DBusProxy.makeProxyWrapper(SystemdLoginUserIface);
+
+function haveSystemd() {
+ return GLib.access("/run/systemd/seats", 0) >= 0;
+}
+
+function versionCompare(required, reference) {
+ required = required.split('.');
+ reference = reference.split('.');
+
+ for (let i = 0; i < required.length; i++) {
+ let requiredInt = parseInt(required[i]);
+ let referenceInt = parseInt(reference[i]);
+ if (requiredInt != referenceInt)
+ return requiredInt < referenceInt;
+ }
+
+ return true;
+}
+
+function canLock() {
+ try {
+ let params = GLib.Variant.new('(ss)', ['org.gnome.DisplayManager.Manager', 'Version']);
+ let result = Gio.DBus.system.call_sync('org.gnome.DisplayManager',
+ '/org/gnome/DisplayManager/Manager',
+ 'org.freedesktop.DBus.Properties',
+ 'Get', params, null,
+ Gio.DBusCallFlags.NONE,
+ -1, null);
+
+ let version = result.deep_unpack()[0].deep_unpack();
+ return haveSystemd() && versionCompare('3.5.91', version);
+ } catch (e) {
+ return false;
+ }
+}
+
+
+async function registerSessionWithGDM() {
+ log("Registering session with GDM");
+ try {
+ await Gio.DBus.system.call(
+ 'org.gnome.DisplayManager',
+ '/org/gnome/DisplayManager/Manager',
+ 'org.gnome.DisplayManager.Manager',
+ 'RegisterSession',
+ GLib.Variant.new('(a{sv})', [{}]), null,
+ Gio.DBusCallFlags.NONE, -1, null);
+ } catch (e) {
+ if (!e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD))
+ log(`Error registering session with GDM: ${e.message}`);
+ else
+ log('Not calling RegisterSession(): method not exported, GDM too old?');
+ }
+}
+
+let _loginManager = null;
+
+/**
+ * getLoginManager:
+ * An abstraction over systemd/logind and ConsoleKit.
+ * @returns {object} - the LoginManager singleton
+ *
+ */
+function getLoginManager() {
+ if (_loginManager == null) {
+ if (haveSystemd())
+ _loginManager = new LoginManagerSystemd();
+ else
+ _loginManager = new LoginManagerDummy();
+ }
+
+ return _loginManager;
+}
+
+var LoginManagerSystemd = class {
+ constructor() {
+ this._proxy = new SystemdLoginManager(Gio.DBus.system,
+ 'org.freedesktop.login1',
+ '/org/freedesktop/login1');
+ this._userProxy = new SystemdLoginUser(Gio.DBus.system,
+ 'org.freedesktop.login1',
+ '/org/freedesktop/login1/user/self');
+ this._proxy.connectSignal('PrepareForSleep',
+ this._prepareForSleep.bind(this));
+ }
+
+ getCurrentSessionProxy(callback) {
+ if (this._currentSession) {
+ callback(this._currentSession);
+ return;
+ }
+
+ let sessionId = GLib.getenv('XDG_SESSION_ID');
+ if (!sessionId) {
+ log('Unset XDG_SESSION_ID, getCurrentSessionProxy() called outside a user session. Asking logind directly.');
+ let [session, objectPath] = this._userProxy.Display;
+ if (session) {
+ log(`Will monitor session ${session}`);
+ sessionId = session;
+ } else {
+ log('Failed to find "Display" session; are we the greeter?');
+
+ for ([session, objectPath] of this._userProxy.Sessions) {
+ let sessionProxy = new SystemdLoginSession(Gio.DBus.system,
+ 'org.freedesktop.login1',
+ objectPath);
+ log(`Considering ${session}, class=${sessionProxy.Class}`);
+ if (sessionProxy.Class == 'greeter') {
+ log(`Yes, will monitor session ${session}`);
+ sessionId = session;
+ break;
+ }
+ }
+
+ if (!sessionId) {
+ log('No, failed to get session from logind.');
+ return;
+ }
+ }
+ }
+
+ this._proxy.GetSessionRemote(sessionId, (result, error) => {
+ if (error) {
+ logError(error, 'Could not get a proxy for the current session');
+ } else {
+ this._currentSession = new SystemdLoginSession(Gio.DBus.system,
+ 'org.freedesktop.login1',
+ result[0]);
+ callback(this._currentSession);
+ }
+ });
+ }
+
+ canSuspend(asyncCallback) {
+ this._proxy.CanSuspendRemote((result, error) => {
+ if (error) {
+ asyncCallback(false, false);
+ } else {
+ let needsAuth = result[0] == 'challenge';
+ let canSuspend = needsAuth || result[0] == 'yes';
+ asyncCallback(canSuspend, needsAuth);
+ }
+ });
+ }
+
+ canRebootToBootLoaderMenu(asyncCallback) {
+ this._proxy.CanRebootToBootLoaderMenuRemote((result, error) => {
+ if (error) {
+ asyncCallback(false, false);
+ } else {
+ const needsAuth = result[0] === 'challenge';
+ const canRebootToBootLoaderMenu = needsAuth || result[0] === 'yes';
+ asyncCallback(canRebootToBootLoaderMenu, needsAuth);
+ }
+ });
+ }
+
+ setRebootToBootLoaderMenu() {
+ /* Parameter is timeout in usec, show to menu for 60 seconds */
+ this._proxy.SetRebootToBootLoaderMenuRemote(60000000);
+ }
+
+ listSessions(asyncCallback) {
+ this._proxy.ListSessionsRemote((result, error) => {
+ if (error)
+ asyncCallback([]);
+ else
+ asyncCallback(result[0]);
+ });
+ }
+
+ suspend() {
+ this._proxy.SuspendRemote(true);
+ }
+
+ async inhibit(reason, callback) {
+ try {
+ const inVariant = new GLib.Variant('(ssss)',
+ ['sleep', 'GNOME Shell', reason, 'delay']);
+ const [outVariant_, fdList] =
+ await this._proxy.call_with_unix_fd_list('Inhibit',
+ inVariant, 0, -1, null, null);
+ const [fd] = fdList.steal_fds();
+ callback(new Gio.UnixInputStream({ fd }));
+ } catch (e) {
+ logError(e, 'Error getting systemd inhibitor');
+ callback(null);
+ }
+ }
+
+ _prepareForSleep(proxy, sender, [aboutToSuspend]) {
+ this.emit('prepare-for-sleep', aboutToSuspend);
+ }
+};
+Signals.addSignalMethods(LoginManagerSystemd.prototype);
+
+var LoginManagerDummy = class {
+ getCurrentSessionProxy(_callback) {
+ // we could return a DummySession object that fakes whatever callers
+ // expect (at the time of writing: connect() and connectSignal()
+ // methods), but just never calling the callback should be safer
+ }
+
+ canSuspend(asyncCallback) {
+ asyncCallback(false, false);
+ }
+
+ canRebootToBootLoaderMenu(asyncCallback) {
+ asyncCallback(false, false);
+ }
+
+ setRebootToBootLoaderMenu() {
+ }
+
+ listSessions(asyncCallback) {
+ asyncCallback([]);
+ }
+
+ suspend() {
+ this.emit('prepare-for-sleep', true);
+ this.emit('prepare-for-sleep', false);
+ }
+
+ inhibit(reason, callback) {
+ callback(null);
+ }
+};
+Signals.addSignalMethods(LoginManagerDummy.prototype);
diff --git a/js/misc/meson.build b/js/misc/meson.build
new file mode 100644
index 0000000..2702c3d
--- /dev/null
+++ b/js/misc/meson.build
@@ -0,0 +1,15 @@
+jsconf = configuration_data()
+jsconf.set('PACKAGE_NAME', meson.project_name())
+jsconf.set('PACKAGE_VERSION', meson.project_version())
+jsconf.set('GETTEXT_PACKAGE', meson.project_name())
+jsconf.set('LIBMUTTER_API_VERSION', mutter_api_version)
+jsconf.set10('HAVE_BLUETOOTH', bt_dep.found())
+jsconf.set10('HAVE_NETWORKMANAGER', have_networkmanager)
+jsconf.set('datadir', datadir)
+jsconf.set('libexecdir', libexecdir)
+
+config_js = configure_file(
+ input: 'config.js.in',
+ output: 'config.js',
+ configuration: jsconf
+)
diff --git a/js/misc/modemManager.js b/js/misc/modemManager.js
new file mode 100644
index 0000000..5f727f3
--- /dev/null
+++ b/js/misc/modemManager.js
@@ -0,0 +1,298 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported ModemBase, ModemGsm, ModemCdma, BroadbandModem */
+
+const { Gio, GObject, NM, NMA } = imports.gi;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+// _getMobileProvidersDatabase:
+//
+// Gets the database of mobile providers, with references between MCCMNC/SID and
+// operator name
+//
+let _mpd;
+function _getMobileProvidersDatabase() {
+ if (_mpd == null) {
+ try {
+ _mpd = new NMA.MobileProvidersDatabase();
+ _mpd.init(null);
+ } catch (e) {
+ log(e.message);
+ _mpd = null;
+ }
+ }
+
+ return _mpd;
+}
+
+// _findProviderForMccMnc:
+// @operatorName: operator name
+// @operatorCode: operator code
+//
+// Given an operator name string (which may not be a real operator name) and an
+// operator code string, tries to find a proper operator name to display.
+//
+function _findProviderForMccMnc(operatorName, operatorCode) {
+ if (operatorName) {
+ if (operatorName.length != 0 &&
+ (operatorName.length > 6 || operatorName.length < 5)) {
+ // this looks like a valid name, i.e. not an MCCMNC (that some
+ // devices return when not yet connected
+ return operatorName;
+ }
+
+ if (isNaN(parseInt(operatorName))) {
+ // name is definitely not a MCCMNC, so it may be a name
+ // after all; return that
+ return operatorName;
+ }
+ }
+
+ let needle;
+ if ((!operatorName || operatorName.length == 0) && operatorCode)
+ needle = operatorCode;
+ else if (operatorName && (operatorName.length == 6 || operatorName.length == 5))
+ needle = operatorName;
+ else // nothing to search
+ return null;
+
+ let mpd = _getMobileProvidersDatabase();
+ if (mpd) {
+ let provider = mpd.lookup_3gpp_mcc_mnc(needle);
+ if (provider)
+ return provider.get_name();
+ }
+ return null;
+}
+
+// _findProviderForSid:
+// @sid: System Identifier of the serving CDMA network
+//
+// Tries to find the operator name corresponding to the given SID
+//
+function _findProviderForSid(sid) {
+ if (!sid)
+ return null;
+
+ let mpd = _getMobileProvidersDatabase();
+ if (mpd) {
+ let provider = mpd.lookup_cdma_sid(sid);
+ if (provider)
+ return provider.get_name();
+ }
+ return null;
+}
+
+
+// ----------------------------------------------------- //
+// Support for the old ModemManager interface (MM < 0.7) //
+// ----------------------------------------------------- //
+
+
+// The following are not the complete interfaces, just the methods we need
+// (or may need in the future)
+
+const ModemGsmNetworkInterface = loadInterfaceXML('org.freedesktop.ModemManager.Modem.Gsm.Network');
+const ModemGsmNetworkProxy = Gio.DBusProxy.makeProxyWrapper(ModemGsmNetworkInterface);
+
+const ModemCdmaInterface = loadInterfaceXML('org.freedesktop.ModemManager.Modem.Cdma');
+const ModemCdmaProxy = Gio.DBusProxy.makeProxyWrapper(ModemCdmaInterface);
+
+var ModemBase = GObject.registerClass({
+ GTypeFlags: GObject.TypeFlags.ABSTRACT,
+ Properties: {
+ 'operator-name': GObject.ParamSpec.string(
+ 'operator-name', 'operator-name', 'operator-name',
+ GObject.ParamFlags.READABLE,
+ null),
+ 'signal-quality': GObject.ParamSpec.int(
+ 'signal-quality', 'signal-quality', 'signal-quality',
+ GObject.ParamFlags.READABLE,
+ 0, 100, 0),
+ },
+}, class ModemBase extends GObject.Object {
+ _init() {
+ super._init();
+ this._operatorName = null;
+ this._signalQuality = 0;
+ }
+
+ // eslint-disable-next-line camelcase
+ get operator_name() {
+ return this._operatorName;
+ }
+
+ // eslint-disable-next-line camelcase
+ get signal_quality() {
+ return this._signalQuality;
+ }
+
+ _setOperatorName(operatorName) {
+ if (this._operatorName == operatorName)
+ return;
+ this._operatorName = operatorName;
+ this.notify('operator-name');
+ }
+
+ _setSignalQuality(signalQuality) {
+ if (this._signalQuality == signalQuality)
+ return;
+ this._signalQuality = signalQuality;
+ this.notify('signal-quality');
+ }
+});
+
+var ModemGsm = GObject.registerClass(
+class ModemGsm extends ModemBase {
+ _init(path) {
+ super._init();
+ this._proxy = new ModemGsmNetworkProxy(Gio.DBus.system, 'org.freedesktop.ModemManager', path);
+
+ // Code is duplicated because the function have different signatures
+ this._proxy.connectSignal('SignalQuality', (proxy, sender, [quality]) => {
+ this._setSignalQuality(quality);
+ });
+ this._proxy.connectSignal('RegistrationInfo', (proxy, sender, [_status, code, name]) => {
+ this._setOperatorName(_findProviderForMccMnc(name, code));
+ });
+ this._proxy.GetRegistrationInfoRemote(([result], err) => {
+ if (err) {
+ log(err);
+ return;
+ }
+
+ let [status_, code, name] = result;
+ this._setOperatorName(_findProviderForMccMnc(name, code));
+ });
+ this._proxy.GetSignalQualityRemote((result, err) => {
+ if (err) {
+ // it will return an error if the device is not connected
+ this._setSignalQuality(0);
+ } else {
+ let [quality] = result;
+ this._setSignalQuality(quality);
+ }
+ });
+ }
+});
+
+var ModemCdma = GObject.registerClass(
+class ModemCdma extends ModemBase {
+ _init(path) {
+ super._init();
+ this._proxy = new ModemCdmaProxy(Gio.DBus.system, 'org.freedesktop.ModemManager', path);
+
+ this._proxy.connectSignal('SignalQuality', (proxy, sender, params) => {
+ this._setSignalQuality(params[0]);
+
+ // receiving this signal means the device got activated
+ // and we can finally call GetServingSystem
+ if (this.operator_name == null)
+ this._refreshServingSystem();
+ });
+ this._proxy.GetSignalQualityRemote((result, err) => {
+ if (err) {
+ // it will return an error if the device is not connected
+ this._setSignalQuality(0);
+ } else {
+ let [quality] = result;
+ this._setSignalQuality(quality);
+ }
+ });
+ }
+
+ _refreshServingSystem() {
+ this._proxy.GetServingSystemRemote(([result], err) => {
+ if (err) {
+ // it will return an error if the device is not connected
+ this._setOperatorName(null);
+ } else {
+ let [bandClass_, band_, sid] = result;
+ this._setOperatorName(_findProviderForSid(sid));
+ }
+ });
+ }
+});
+
+
+// ------------------------------------------------------- //
+// Support for the new ModemManager1 interface (MM >= 0.7) //
+// ------------------------------------------------------- //
+
+const BroadbandModemInterface = loadInterfaceXML('org.freedesktop.ModemManager1.Modem');
+const BroadbandModemProxy = Gio.DBusProxy.makeProxyWrapper(BroadbandModemInterface);
+
+const BroadbandModem3gppInterface = loadInterfaceXML('org.freedesktop.ModemManager1.Modem.Modem3gpp');
+const BroadbandModem3gppProxy = Gio.DBusProxy.makeProxyWrapper(BroadbandModem3gppInterface);
+
+const BroadbandModemCdmaInterface = loadInterfaceXML('org.freedesktop.ModemManager1.Modem.ModemCdma');
+const BroadbandModemCdmaProxy = Gio.DBusProxy.makeProxyWrapper(BroadbandModemCdmaInterface);
+
+var BroadbandModem = GObject.registerClass({
+ Properties: {
+ 'capabilities': GObject.ParamSpec.flags(
+ 'capabilities', 'capabilities', 'capabilities',
+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+ NM.DeviceModemCapabilities.$gtype,
+ NM.DeviceModemCapabilities.NONE),
+ },
+}, class BroadbandModem extends ModemBase {
+ _init(path, capabilities) {
+ super._init({ capabilities });
+ this._proxy = new BroadbandModemProxy(Gio.DBus.system, 'org.freedesktop.ModemManager1', path);
+ this._proxy_3gpp = new BroadbandModem3gppProxy(Gio.DBus.system, 'org.freedesktop.ModemManager1', path);
+ this._proxy_cdma = new BroadbandModemCdmaProxy(Gio.DBus.system, 'org.freedesktop.ModemManager1', path);
+
+ this._proxy.connect('g-properties-changed', (proxy, properties) => {
+ if ('SignalQuality' in properties.deep_unpack())
+ this._reloadSignalQuality();
+ });
+ this._reloadSignalQuality();
+
+ this._proxy_3gpp.connect('g-properties-changed', (proxy, properties) => {
+ let unpacked = properties.deep_unpack();
+ if ('OperatorName' in unpacked || 'OperatorCode' in unpacked)
+ this._reload3gppOperatorName();
+ });
+ this._reload3gppOperatorName();
+
+ this._proxy_cdma.connect('g-properties-changed', (proxy, properties) => {
+ let unpacked = properties.deep_unpack();
+ if ('Nid' in unpacked || 'Sid' in unpacked)
+ this._reloadCdmaOperatorName();
+ });
+ this._reloadCdmaOperatorName();
+ }
+
+ _reloadSignalQuality() {
+ let [quality, recent_] = this._proxy.SignalQuality;
+ this._setSignalQuality(quality);
+ }
+
+ _reloadOperatorName() {
+ let newName = "";
+ if (this.operator_name_3gpp && this.operator_name_3gpp.length > 0)
+ newName += this.operator_name_3gpp;
+
+ if (this.operator_name_cdma && this.operator_name_cdma.length > 0) {
+ if (newName != "")
+ newName += ", ";
+ newName += this.operator_name_cdma;
+ }
+
+ this._setOperatorName(newName);
+ }
+
+ _reload3gppOperatorName() {
+ let name = this._proxy_3gpp.OperatorName;
+ let code = this._proxy_3gpp.OperatorCode;
+ this.operator_name_3gpp = _findProviderForMccMnc(name, code);
+ this._reloadOperatorName();
+ }
+
+ _reloadCdmaOperatorName() {
+ let sid = this._proxy_cdma.Sid;
+ this.operator_name_cdma = _findProviderForSid(sid);
+ this._reloadOperatorName();
+ }
+});
diff --git a/js/misc/objectManager.js b/js/misc/objectManager.js
new file mode 100644
index 0000000..4c4e0b6
--- /dev/null
+++ b/js/misc/objectManager.js
@@ -0,0 +1,285 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const { Gio, GLib } = imports.gi;
+const Params = imports.misc.params;
+const Signals = imports.signals;
+
+// Specified in the D-Bus specification here:
+// http://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager
+const ObjectManagerIface = `
+<node>
+<interface name="org.freedesktop.DBus.ObjectManager">
+ <method name="GetManagedObjects">
+ <arg name="objects" type="a{oa{sa{sv}}}" direction="out"/>
+ </method>
+ <signal name="InterfacesAdded">
+ <arg name="objectPath" type="o"/>
+ <arg name="interfaces" type="a{sa{sv}}" />
+ </signal>
+ <signal name="InterfacesRemoved">
+ <arg name="objectPath" type="o"/>
+ <arg name="interfaces" type="as" />
+ </signal>
+</interface>
+</node>`;
+
+const ObjectManagerInfo = Gio.DBusInterfaceInfo.new_for_xml(ObjectManagerIface);
+
+var ObjectManager = class {
+ constructor(params) {
+ params = Params.parse(params, { connection: null,
+ name: null,
+ objectPath: null,
+ knownInterfaces: null,
+ cancellable: null,
+ onLoaded: null });
+
+ this._connection = params.connection;
+ this._serviceName = params.name;
+ this._managerPath = params.objectPath;
+ this._cancellable = params.cancellable;
+
+ this._managerProxy = new Gio.DBusProxy({ g_connection: this._connection,
+ g_interface_name: ObjectManagerInfo.name,
+ g_interface_info: ObjectManagerInfo,
+ g_name: this._serviceName,
+ g_object_path: this._managerPath,
+ g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START });
+
+ this._interfaceInfos = {};
+ this._objects = {};
+ this._interfaces = {};
+ this._onLoaded = params.onLoaded;
+
+ if (params.knownInterfaces)
+ this._registerInterfaces(params.knownInterfaces);
+
+ // Start out inhibiting load until at least the proxy
+ // manager is loaded and the remote objects are fetched
+ this._numLoadInhibitors = 1;
+ this._initManagerProxy();
+ }
+
+ _tryToCompleteLoad() {
+ if (this._numLoadInhibitors == 0)
+ return;
+
+ this._numLoadInhibitors--;
+ if (this._numLoadInhibitors == 0) {
+ if (this._onLoaded)
+ this._onLoaded();
+ }
+ }
+
+ async _addInterface(objectPath, interfaceName, onFinished) {
+ let info = this._interfaceInfos[interfaceName];
+
+ if (!info) {
+ if (onFinished)
+ onFinished();
+ return;
+ }
+
+ let proxy = new Gio.DBusProxy({ g_connection: this._connection,
+ g_name: this._serviceName,
+ g_object_path: objectPath,
+ g_interface_name: interfaceName,
+ g_interface_info: info,
+ g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START });
+
+ try {
+ await proxy.init_async(GLib.PRIORITY_DEFAULT, this._cancellable);
+ } catch (e) {
+ logError(e, `could not initialize proxy for interface ${interfaceName}`);
+
+ if (onFinished)
+ onFinished();
+ return;
+ }
+
+ let isNewObject;
+ if (!this._objects[objectPath]) {
+ this._objects[objectPath] = {};
+ isNewObject = true;
+ } else {
+ isNewObject = false;
+ }
+
+ this._objects[objectPath][interfaceName] = proxy;
+
+ if (!this._interfaces[interfaceName])
+ this._interfaces[interfaceName] = [];
+
+ this._interfaces[interfaceName].push(proxy);
+
+ if (isNewObject)
+ this.emit('object-added', objectPath);
+
+ this.emit('interface-added', interfaceName, proxy);
+
+ if (onFinished)
+ onFinished();
+ }
+
+ _removeInterface(objectPath, interfaceName) {
+ if (!this._objects[objectPath])
+ return;
+
+ let proxy = this._objects[objectPath][interfaceName];
+
+ if (this._interfaces[interfaceName]) {
+ let index = this._interfaces[interfaceName].indexOf(proxy);
+
+ if (index >= 0)
+ this._interfaces[interfaceName].splice(index, 1);
+
+ if (this._interfaces[interfaceName].length == 0)
+ delete this._interfaces[interfaceName];
+ }
+
+ this.emit('interface-removed', interfaceName, proxy);
+
+ this._objects[objectPath][interfaceName] = null;
+
+ if (Object.keys(this._objects[objectPath]).length == 0) {
+ delete this._objects[objectPath];
+ this.emit('object-removed', objectPath);
+ }
+ }
+
+ async _initManagerProxy() {
+ try {
+ await this._managerProxy.init_async(
+ GLib.PRIORITY_DEFAULT, this._cancellable);
+ } catch (e) {
+ logError(e, `could not initialize object manager for object ${this._serviceName}`);
+
+ this._tryToCompleteLoad();
+ return;
+ }
+
+ this._managerProxy.connectSignal('InterfacesAdded',
+ (objectManager, sender, [objectPath, interfaces]) => {
+ let interfaceNames = Object.keys(interfaces);
+ for (let i = 0; i < interfaceNames.length; i++)
+ this._addInterface(objectPath, interfaceNames[i]);
+ });
+ this._managerProxy.connectSignal('InterfacesRemoved',
+ (objectManager, sender, [objectPath, interfaceNames]) => {
+ for (let i = 0; i < interfaceNames.length; i++)
+ this._removeInterface(objectPath, interfaceNames[i]);
+ });
+
+ if (Object.keys(this._interfaceInfos).length == 0) {
+ this._tryToCompleteLoad();
+ return;
+ }
+
+ this._managerProxy.connect('notify::g-name-owner', () => {
+ if (this._managerProxy.g_name_owner)
+ this._onNameAppeared();
+ else
+ this._onNameVanished();
+ });
+
+ if (this._managerProxy.g_name_owner)
+ this._onNameAppeared();
+ }
+
+ _onNameAppeared() {
+ this._managerProxy.GetManagedObjectsRemote((result, error) => {
+ if (!result) {
+ if (error)
+ logError(error, `could not get remote objects for service ${this._serviceName} path ${this._managerPath}`);
+
+ this._tryToCompleteLoad();
+ return;
+ }
+
+ let [objects] = result;
+
+ if (!objects) {
+ this._tryToCompleteLoad();
+ return;
+ }
+
+ let objectPaths = Object.keys(objects);
+ for (let i = 0; i < objectPaths.length; i++) {
+ let objectPath = objectPaths[i];
+ let object = objects[objectPath];
+
+ let interfaceNames = Object.getOwnPropertyNames(object);
+ for (let j = 0; j < interfaceNames.length; j++) {
+ let interfaceName = interfaceNames[j];
+
+ // Prevent load from completing until the interface is loaded
+ this._numLoadInhibitors++;
+ this._addInterface(objectPath,
+ interfaceName,
+ this._tryToCompleteLoad.bind(this));
+ }
+ }
+ this._tryToCompleteLoad();
+ });
+ }
+
+ _onNameVanished() {
+ let objectPaths = Object.keys(this._objects);
+ for (let i = 0; i < objectPaths.length; i++) {
+ let objectPath = objectPaths[i];
+ let object = this._objects[objectPath];
+
+ let interfaceNames = Object.keys(object);
+ for (let j = 0; j < interfaceNames.length; j++) {
+ let interfaceName = interfaceNames[j];
+
+ if (object[interfaceName])
+ this._removeInterface(objectPath, interfaceName);
+ }
+ }
+ }
+
+ _registerInterfaces(interfaces) {
+ for (let i = 0; i < interfaces.length; i++) {
+ let info = Gio.DBusInterfaceInfo.new_for_xml(interfaces[i]);
+ this._interfaceInfos[info.name] = info;
+ }
+ }
+
+ getProxy(objectPath, interfaceName) {
+ let object = this._objects[objectPath];
+
+ if (!object)
+ return null;
+
+ return object[interfaceName];
+ }
+
+ getProxiesForInterface(interfaceName) {
+ let proxyList = this._interfaces[interfaceName];
+
+ if (!proxyList)
+ return [];
+
+ return proxyList;
+ }
+
+ getAllProxies() {
+ let proxies = [];
+
+ let objectPaths = Object.keys(this._objects);
+ for (let i = 0; i < objectPaths.length; i++) {
+ let object = this._objects[objectPaths];
+
+ let interfaceNames = Object.keys(object);
+ for (let j = 0; j < interfaceNames.length; j++) {
+ let interfaceName = interfaceNames[j];
+ if (object[interfaceName])
+ proxies.push(object(interfaceName));
+ }
+ }
+
+ return proxies;
+ }
+};
+Signals.addSignalMethods(ObjectManager.prototype);
diff --git a/js/misc/params.js b/js/misc/params.js
new file mode 100644
index 0000000..817d66c
--- /dev/null
+++ b/js/misc/params.js
@@ -0,0 +1,28 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported parse */
+
+// parse:
+// @params: caller-provided parameter object, or %null
+// @defaults-provided defaults object
+// @allowExtras: whether or not to allow properties not in @default
+//
+// Examines @params and fills in default values from @defaults for
+// any properties in @defaults that don't appear in @params. If
+// @allowExtras is not %true, it will throw an error if @params
+// contains any properties that aren't in @defaults.
+//
+// If @params is %null, this returns the values from @defaults.
+//
+// Return value: a new object, containing the merged parameters from
+// @params and @defaults
+function parse(params = {}, defaults, allowExtras) {
+ if (!allowExtras) {
+ for (let prop in params) {
+ if (!(prop in defaults))
+ throw new Error(`Unrecognized parameter "${prop}"`);
+ }
+ }
+
+ let defaultsCopy = Object.assign({}, defaults);
+ return Object.assign(defaultsCopy, params);
+}
diff --git a/js/misc/parentalControlsManager.js b/js/misc/parentalControlsManager.js
new file mode 100644
index 0000000..fc1e7ce
--- /dev/null
+++ b/js/misc/parentalControlsManager.js
@@ -0,0 +1,146 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+//
+// Copyright (C) 2018, 2019, 2020 Endless Mobile, Inc.
+//
+// This is a GNOME Shell component to wrap the interactions over
+// D-Bus with the malcontent library.
+//
+// Licensed under the GNU General Public License Version 2
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+/* exported getDefault */
+
+const { Gio, GObject, Shell } = imports.gi;
+
+// We require libmalcontent ā‰„ 0.6.0
+const HAVE_MALCONTENT = imports.package.checkSymbol(
+ 'Malcontent', '0', 'ManagerGetValueFlags');
+
+var Malcontent = null;
+if (HAVE_MALCONTENT) {
+ Malcontent = imports.gi.Malcontent;
+ Gio._promisify(Malcontent.Manager.prototype, 'get_app_filter_async', 'get_app_filter_finish');
+}
+
+let _singleton = null;
+
+function getDefault() {
+ if (_singleton === null)
+ _singleton = new ParentalControlsManager();
+
+ return _singleton;
+}
+
+// A manager class which provides cached access to the constructing userā€™s
+// parental controls settings. Itā€™s possible for the userā€™s parental controls
+// to change at runtime if the Parental Controls application is used by an
+// administrator from within the userā€™s session.
+var ParentalControlsManager = GObject.registerClass({
+ Signals: {
+ 'app-filter-changed': {},
+ },
+}, class ParentalControlsManager extends GObject.Object {
+ _init() {
+ super._init();
+
+ this._initialized = false;
+ this._disabled = false;
+ this._appFilter = null;
+
+ this._initializeManager();
+ }
+
+ async _initializeManager() {
+ if (!HAVE_MALCONTENT) {
+ log('Skipping parental controls support as itā€™s disabled');
+ this._initialized = true;
+ this.emit('app-filter-changed');
+ return;
+ }
+
+ log(`Getting parental controls for user ${Shell.util_get_uid()}`);
+ try {
+ const connection = await Gio.DBus.get(Gio.BusType.SYSTEM, null);
+ this._manager = new Malcontent.Manager({ connection });
+ this._appFilter = await this._manager.get_app_filter_async(
+ Shell.util_get_uid(),
+ Malcontent.ManagerGetValueFlags.NONE,
+ null);
+ } catch (e) {
+ if (e.matches(Malcontent.ManagerError, Malcontent.ManagerError.DISABLED)) {
+ log('Parental controls globally disabled');
+ this._disabled = true;
+ } else {
+ logError(e, 'Failed to get parental controls settings');
+ return;
+ }
+ }
+
+ this._manager.connect('app-filter-changed', this._onAppFilterChanged.bind(this));
+
+ // Signal initialisation is complete.
+ this._initialized = true;
+ this.emit('app-filter-changed');
+ }
+
+ async _onAppFilterChanged(manager, uid) {
+ // Emit 'changed' signal only if app-filter is changed for currently logged-in user.
+ let currentUid = Shell.util_get_uid();
+ if (currentUid !== uid)
+ return;
+
+ try {
+ this._appFilter = await this._manager.get_app_filter_async(
+ currentUid,
+ Malcontent.ManagerGetValueFlags.NONE,
+ null);
+ this.emit('app-filter-changed');
+ } catch (e) {
+ // Log an error and keep the old app filter.
+ logError(e, `Failed to get new MctAppFilter for uid ${Shell.util_get_uid()} on app-filter-changed`);
+ }
+ }
+
+ get initialized() {
+ return this._initialized;
+ }
+
+ // Calculate whether the given app (a Gio.DesktopAppInfo) should be shown
+ // on the desktop, in search results, etc. The app should be shown if:
+ // - The .desktop file doesnā€™t say it should be hidden.
+ // - The executable from the .desktop fileā€™s Exec line isnā€™t denied in
+ // the userā€™s parental controls.
+ // - None of the flatpak app IDs from the X-Flatpak and the
+ // X-Flatpak-RenamedFrom lines are denied in the userā€™s parental
+ // controls.
+ shouldShowApp(appInfo) {
+ // Quick decision?
+ if (!appInfo.should_show())
+ return false;
+
+ // Are parental controls enabled (at configure time or runtime)?
+ if (!HAVE_MALCONTENT || this._disabled)
+ return true;
+
+ // Have we finished initialising yet?
+ if (!this.initialized) {
+ log(`Warning: Hiding app because parental controls not yet initialised: ${appInfo.get_id()}`);
+ return false;
+ }
+
+ return this._appFilter.is_appinfo_allowed(appInfo);
+ }
+});
diff --git a/js/misc/permissionStore.js b/js/misc/permissionStore.js
new file mode 100644
index 0000000..46c5d54
--- /dev/null
+++ b/js/misc/permissionStore.js
@@ -0,0 +1,16 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported PermissionStore */
+
+const Gio = imports.gi.Gio;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const PermissionStoreIface = loadInterfaceXML('org.freedesktop.impl.portal.PermissionStore');
+const PermissionStoreProxy = Gio.DBusProxy.makeProxyWrapper(PermissionStoreIface);
+
+function PermissionStore(initCallback, cancellable) {
+ return new PermissionStoreProxy(Gio.DBus.session,
+ 'org.freedesktop.impl.portal.PermissionStore',
+ '/org/freedesktop/impl/portal/PermissionStore',
+ initCallback, cancellable);
+}
diff --git a/js/misc/smartcardManager.js b/js/misc/smartcardManager.js
new file mode 100644
index 0000000..e1d0f1b
--- /dev/null
+++ b/js/misc/smartcardManager.js
@@ -0,0 +1,116 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported getSmartcardManager */
+
+const Gio = imports.gi.Gio;
+const Signals = imports.signals;
+
+const ObjectManager = imports.misc.objectManager;
+
+const SmartcardTokenIface = `
+<node>
+<interface name="org.gnome.SettingsDaemon.Smartcard.Token">
+ <property name="Name" type="s" access="read"/>
+ <property name="Driver" type="o" access="read"/>
+ <property name="IsInserted" type="b" access="read"/>
+ <property name="UsedToLogin" type="b" access="read"/>
+</interface>
+</node>`;
+
+let _smartcardManager = null;
+
+function getSmartcardManager() {
+ if (_smartcardManager == null)
+ _smartcardManager = new SmartcardManager();
+
+ return _smartcardManager;
+}
+
+var SmartcardManager = class {
+ constructor() {
+ this._objectManager = new ObjectManager.ObjectManager({ connection: Gio.DBus.session,
+ name: "org.gnome.SettingsDaemon.Smartcard",
+ objectPath: '/org/gnome/SettingsDaemon/Smartcard',
+ knownInterfaces: [SmartcardTokenIface],
+ onLoaded: this._onLoaded.bind(this) });
+ this._insertedTokens = {};
+ this._loginToken = null;
+ }
+
+ _onLoaded() {
+ let tokens = this._objectManager.getProxiesForInterface('org.gnome.SettingsDaemon.Smartcard.Token');
+
+ for (let i = 0; i < tokens.length; i++)
+ this._addToken(tokens[i]);
+
+ this._objectManager.connect('interface-added', (objectManager, interfaceName, proxy) => {
+ if (interfaceName == 'org.gnome.SettingsDaemon.Smartcard.Token')
+ this._addToken(proxy);
+ });
+
+ this._objectManager.connect('interface-removed', (objectManager, interfaceName, proxy) => {
+ if (interfaceName == 'org.gnome.SettingsDaemon.Smartcard.Token')
+ this._removeToken(proxy);
+ });
+ }
+
+ _updateToken(token) {
+ let objectPath = token.get_object_path();
+
+ delete this._insertedTokens[objectPath];
+
+ if (token.IsInserted)
+ this._insertedTokens[objectPath] = token;
+
+ if (token.UsedToLogin)
+ this._loginToken = token;
+ }
+
+ _addToken(token) {
+ this._updateToken(token);
+
+ token.connect('g-properties-changed', (proxy, properties) => {
+ if ('IsInserted' in properties.deep_unpack()) {
+ this._updateToken(token);
+
+ if (token.IsInserted)
+ this.emit('smartcard-inserted', token);
+ else
+ this.emit('smartcard-removed', token);
+ }
+ });
+
+ // Emit a smartcard-inserted at startup if it's already plugged in
+ if (token.IsInserted)
+ this.emit('smartcard-inserted', token);
+ }
+
+ _removeToken(token) {
+ let objectPath = token.get_object_path();
+
+ if (this._insertedTokens[objectPath] == token) {
+ delete this._insertedTokens[objectPath];
+ this.emit('smartcard-removed', token);
+ }
+
+ if (this._loginToken == token)
+ this._loginToken = null;
+
+ token.disconnectAll();
+ }
+
+ hasInsertedTokens() {
+ return Object.keys(this._insertedTokens).length > 0;
+ }
+
+ hasInsertedLoginToken() {
+ if (!this._loginToken)
+ return false;
+
+ if (!this._loginToken.IsInserted)
+ return false;
+
+ return true;
+ }
+
+};
+Signals.addSignalMethods(SmartcardManager.prototype);
diff --git a/js/misc/systemActions.js b/js/misc/systemActions.js
new file mode 100644
index 0000000..1cd6576
--- /dev/null
+++ b/js/misc/systemActions.js
@@ -0,0 +1,457 @@
+/* exported getDefault */
+const { AccountsService, Clutter, Gdm, Gio, GLib, GObject, Meta } = imports.gi;
+
+const GnomeSession = imports.misc.gnomeSession;
+const LoginManager = imports.misc.loginManager;
+const Main = imports.ui.main;
+
+const LOCKDOWN_SCHEMA = 'org.gnome.desktop.lockdown';
+const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen';
+const DISABLE_USER_SWITCH_KEY = 'disable-user-switching';
+const DISABLE_LOCK_SCREEN_KEY = 'disable-lock-screen';
+const DISABLE_LOG_OUT_KEY = 'disable-log-out';
+const DISABLE_RESTART_KEY = 'disable-restart-buttons';
+const ALWAYS_SHOW_LOG_OUT_KEY = 'always-show-log-out';
+
+const POWER_OFF_ACTION_ID = 'power-off';
+const RESTART_ACTION_ID = 'restart';
+const LOCK_SCREEN_ACTION_ID = 'lock-screen';
+const LOGOUT_ACTION_ID = 'logout';
+const SUSPEND_ACTION_ID = 'suspend';
+const SWITCH_USER_ACTION_ID = 'switch-user';
+const LOCK_ORIENTATION_ACTION_ID = 'lock-orientation';
+
+let _singleton = null;
+
+function getDefault() {
+ if (_singleton == null)
+ _singleton = new SystemActions();
+
+ return _singleton;
+}
+
+const SystemActions = GObject.registerClass({
+ Properties: {
+ 'can-power-off': GObject.ParamSpec.boolean(
+ 'can-power-off', 'can-power-off', 'can-power-off',
+ GObject.ParamFlags.READABLE,
+ false),
+ 'can-restart': GObject.ParamSpec.boolean(
+ 'can-restart', 'can-restart', 'can-restart',
+ GObject.ParamFlags.READABLE,
+ false),
+ 'can-suspend': GObject.ParamSpec.boolean(
+ 'can-suspend', 'can-suspend', 'can-suspend',
+ GObject.ParamFlags.READABLE,
+ false),
+ 'can-lock-screen': GObject.ParamSpec.boolean(
+ 'can-lock-screen', 'can-lock-screen', 'can-lock-screen',
+ GObject.ParamFlags.READABLE,
+ false),
+ 'can-switch-user': GObject.ParamSpec.boolean(
+ 'can-switch-user', 'can-switch-user', 'can-switch-user',
+ GObject.ParamFlags.READABLE,
+ false),
+ 'can-logout': GObject.ParamSpec.boolean(
+ 'can-logout', 'can-logout', 'can-logout',
+ GObject.ParamFlags.READABLE,
+ false),
+ 'can-lock-orientation': GObject.ParamSpec.boolean(
+ 'can-lock-orientation', 'can-lock-orientation', 'can-lock-orientation',
+ GObject.ParamFlags.READABLE,
+ false),
+ 'orientation-lock-icon': GObject.ParamSpec.string(
+ 'orientation-lock-icon', 'orientation-lock-icon', 'orientation-lock-icon',
+ GObject.ParamFlags.READWRITE,
+ null),
+ },
+}, class SystemActions extends GObject.Object {
+ _init() {
+ super._init();
+
+ this._canHavePowerOff = true;
+ this._canHaveSuspend = true;
+
+ function tokenizeKeywords(keywords) {
+ return keywords.split(';').map(keyword => GLib.str_tokenize_and_fold(keyword, null)).flat(2);
+ }
+
+ this._actions = new Map();
+ this._actions.set(POWER_OFF_ACTION_ID, {
+ // Translators: The name of the power-off action in search
+ name: C_("search-result", "Power Off"),
+ iconName: 'system-shutdown-symbolic',
+ // Translators: A list of keywords that match the power-off action, separated by semicolons
+ keywords: tokenizeKeywords(_('power off;shutdown;halt;stop')),
+ available: false,
+ });
+ this._actions.set(RESTART_ACTION_ID, {
+ // Translators: The name of the restart action in search
+ name: C_('search-result', 'Restart'),
+ iconName: 'system-reboot-symbolic',
+ // Translators: A list of keywords that match the restart action, separated by semicolons
+ keywords: tokenizeKeywords(_('reboot;restart;')),
+ available: false,
+ });
+ this._actions.set(LOCK_SCREEN_ACTION_ID, {
+ // Translators: The name of the lock screen action in search
+ name: C_("search-result", "Lock Screen"),
+ iconName: 'system-lock-screen-symbolic',
+ // Translators: A list of keywords that match the lock screen action, separated by semicolons
+ keywords: tokenizeKeywords(_('lock screen')),
+ available: false,
+ });
+ this._actions.set(LOGOUT_ACTION_ID, {
+ // Translators: The name of the logout action in search
+ name: C_("search-result", "Log Out"),
+ iconName: 'system-log-out-symbolic',
+ // Translators: A list of keywords that match the logout action, separated by semicolons
+ keywords: tokenizeKeywords(_('logout;log out;sign off')),
+ available: false,
+ });
+ this._actions.set(SUSPEND_ACTION_ID, {
+ // Translators: The name of the suspend action in search
+ name: C_("search-result", "Suspend"),
+ iconName: 'media-playback-pause-symbolic',
+ // Translators: A list of keywords that match the suspend action, separated by semicolons
+ keywords: tokenizeKeywords(_('suspend;sleep')),
+ available: false,
+ });
+ this._actions.set(SWITCH_USER_ACTION_ID, {
+ // Translators: The name of the switch user action in search
+ name: C_("search-result", "Switch User"),
+ iconName: 'system-switch-user-symbolic',
+ // Translators: A list of keywords that match the switch user action, separated by semicolons
+ keywords: tokenizeKeywords(_('switch user')),
+ available: false,
+ });
+ this._actions.set(LOCK_ORIENTATION_ACTION_ID, {
+ name: '',
+ iconName: '',
+ // Translators: A list of keywords that match the lock orientation action, separated by semicolons
+ keywords: tokenizeKeywords(_('lock orientation;unlock orientation;screen;rotation')),
+ available: false,
+ });
+
+ this._loginScreenSettings = new Gio.Settings({ schema_id: LOGIN_SCREEN_SCHEMA });
+ this._lockdownSettings = new Gio.Settings({ schema_id: LOCKDOWN_SCHEMA });
+ this._orientationSettings = new Gio.Settings({ schema_id: 'org.gnome.settings-daemon.peripherals.touchscreen' });
+
+ this._session = new GnomeSession.SessionManager();
+ this._loginManager = LoginManager.getLoginManager();
+ this._monitorManager = Meta.MonitorManager.get();
+
+ this._userManager = AccountsService.UserManager.get_default();
+
+ this._userManager.connect('notify::is-loaded',
+ () => this._updateMultiUser());
+ this._userManager.connect('notify::has-multiple-users',
+ () => this._updateMultiUser());
+ this._userManager.connect('user-added',
+ () => this._updateMultiUser());
+ this._userManager.connect('user-removed',
+ () => this._updateMultiUser());
+
+ this._lockdownSettings.connect('changed::%s'.format(DISABLE_USER_SWITCH_KEY),
+ () => this._updateSwitchUser());
+ this._lockdownSettings.connect('changed::%s'.format(DISABLE_LOG_OUT_KEY),
+ () => this._updateLogout());
+ global.settings.connect('changed::%s'.format(ALWAYS_SHOW_LOG_OUT_KEY),
+ () => this._updateLogout());
+
+ this._lockdownSettings.connect('changed::%s'.format(DISABLE_LOCK_SCREEN_KEY),
+ () => this._updateLockScreen());
+
+ this._lockdownSettings.connect('changed::%s'.format(DISABLE_LOG_OUT_KEY),
+ () => this._updateHaveShutdown());
+
+ this.forceUpdate();
+
+ this._orientationSettings.connect('changed::orientation-lock', () => {
+ this._updateOrientationLock();
+ this._updateOrientationLockStatus();
+ });
+ Main.layoutManager.connect('monitors-changed',
+ () => this._updateOrientationLock());
+ this._monitorManager.connect('notify::panel-orientation-managed',
+ () => this._updateOrientationLock());
+ this._updateOrientationLock();
+ this._updateOrientationLockStatus();
+
+ Main.sessionMode.connect('updated', () => this._sessionUpdated());
+ this._sessionUpdated();
+ }
+
+ // eslint-disable-next-line camelcase
+ get can_power_off() {
+ return this._actions.get(POWER_OFF_ACTION_ID).available;
+ }
+
+ // eslint-disable-next-line camelcase
+ get can_restart() {
+ return this._actions.get(RESTART_ACTION_ID).available;
+ }
+
+ // eslint-disable-next-line camelcase
+ get can_suspend() {
+ return this._actions.get(SUSPEND_ACTION_ID).available;
+ }
+
+ // eslint-disable-next-line camelcase
+ get can_lock_screen() {
+ return this._actions.get(LOCK_SCREEN_ACTION_ID).available;
+ }
+
+ // eslint-disable-next-line camelcase
+ get can_switch_user() {
+ return this._actions.get(SWITCH_USER_ACTION_ID).available;
+ }
+
+ // eslint-disable-next-line camelcase
+ get can_logout() {
+ return this._actions.get(LOGOUT_ACTION_ID).available;
+ }
+
+ // eslint-disable-next-line camelcase
+ get can_lock_orientation() {
+ return this._actions.get(LOCK_ORIENTATION_ACTION_ID).available;
+ }
+
+ // eslint-disable-next-line camelcase
+ get orientation_lock_icon() {
+ return this._actions.get(LOCK_ORIENTATION_ACTION_ID).iconName;
+ }
+
+ _updateOrientationLock() {
+ const available = this._monitorManager.get_panel_orientation_managed();
+
+ this._actions.get(LOCK_ORIENTATION_ACTION_ID).available = available;
+
+ this.notify('can-lock-orientation');
+ }
+
+ _updateOrientationLockStatus() {
+ let locked = this._orientationSettings.get_boolean('orientation-lock');
+ let action = this._actions.get(LOCK_ORIENTATION_ACTION_ID);
+
+ // Translators: The name of the lock orientation action in search
+ // and in the system status menu
+ let name = locked
+ ? C_('search-result', 'Unlock Screen Rotation')
+ : C_('search-result', 'Lock Screen Rotation');
+ let iconName = locked
+ ? 'rotation-locked-symbolic'
+ : 'rotation-allowed-symbolic';
+
+ action.name = name;
+ action.iconName = iconName;
+
+ this.notify('orientation-lock-icon');
+ }
+
+ _sessionUpdated() {
+ this._updateLockScreen();
+ this._updatePowerOff();
+ this._updateSuspend();
+ this._updateMultiUser();
+ }
+
+ forceUpdate() {
+ // Whether those actions are available or not depends on both lockdown
+ // settings and Polkit policy - we don't get change notifications for the
+ // latter, so their value may be outdated; force an update now
+ this._updateHaveShutdown();
+ this._updateHaveSuspend();
+ }
+
+ getMatchingActions(terms) {
+ // terms is a list of strings
+ terms = terms.map(
+ term => GLib.str_tokenize_and_fold(term, null)[0]).flat(2);
+
+ // tokenizing may return an empty array
+ if (terms.length === 0)
+ return [];
+
+ let results = [];
+
+ for (let [key, { available, keywords }] of this._actions) {
+ if (available && terms.every(t => keywords.some(k => k.startsWith(t))))
+ results.push(key);
+ }
+
+ return results;
+ }
+
+ getName(id) {
+ return this._actions.get(id).name;
+ }
+
+ getIconName(id) {
+ return this._actions.get(id).iconName;
+ }
+
+ activateAction(id) {
+ switch (id) {
+ case POWER_OFF_ACTION_ID:
+ this.activatePowerOff();
+ break;
+ case RESTART_ACTION_ID:
+ this.activateRestart();
+ break;
+ case LOCK_SCREEN_ACTION_ID:
+ this.activateLockScreen();
+ break;
+ case LOGOUT_ACTION_ID:
+ this.activateLogout();
+ break;
+ case SUSPEND_ACTION_ID:
+ this.activateSuspend();
+ break;
+ case SWITCH_USER_ACTION_ID:
+ this.activateSwitchUser();
+ break;
+ case LOCK_ORIENTATION_ACTION_ID:
+ this.activateLockOrientation();
+ break;
+ }
+ }
+
+ _updateLockScreen() {
+ let showLock = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+ let allowLockScreen = !this._lockdownSettings.get_boolean(DISABLE_LOCK_SCREEN_KEY);
+ this._actions.get(LOCK_SCREEN_ACTION_ID).available = showLock && allowLockScreen && LoginManager.canLock();
+ this.notify('can-lock-screen');
+ }
+
+ _updateHaveShutdown() {
+ this._session.CanShutdownRemote((result, error) => {
+ if (error)
+ return;
+
+ this._canHavePowerOff = result[0];
+ this._updatePowerOff();
+ });
+ }
+
+ _updatePowerOff() {
+ let disabled = Main.sessionMode.isLocked ||
+ (Main.sessionMode.isGreeter &&
+ this._loginScreenSettings.get_boolean(DISABLE_RESTART_KEY));
+ this._actions.get(POWER_OFF_ACTION_ID).available = this._canHavePowerOff && !disabled;
+ this.notify('can-power-off');
+
+ this._actions.get(RESTART_ACTION_ID).available = this._canHavePowerOff && !disabled;
+ this.notify('can-restart');
+ }
+
+ _updateHaveSuspend() {
+ this._loginManager.canSuspend(
+ (canSuspend, needsAuth) => {
+ this._canHaveSuspend = canSuspend;
+ this._suspendNeedsAuth = needsAuth;
+ this._updateSuspend();
+ });
+ }
+
+ _updateSuspend() {
+ let disabled = (Main.sessionMode.isLocked &&
+ this._suspendNeedsAuth) ||
+ (Main.sessionMode.isGreeter &&
+ this._loginScreenSettings.get_boolean(DISABLE_RESTART_KEY));
+ this._actions.get(SUSPEND_ACTION_ID).available = this._canHaveSuspend && !disabled;
+ this.notify('can-suspend');
+ }
+
+ _updateMultiUser() {
+ this._updateLogout();
+ this._updateSwitchUser();
+ }
+
+ _updateSwitchUser() {
+ let allowSwitch = !this._lockdownSettings.get_boolean(DISABLE_USER_SWITCH_KEY);
+ let multiUser = this._userManager.can_switch() && this._userManager.has_multiple_users;
+ let shouldShowInMode = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+
+ let visible = allowSwitch && multiUser && shouldShowInMode;
+ this._actions.get(SWITCH_USER_ACTION_ID).available = visible;
+ this.notify('can-switch-user');
+
+ return visible;
+ }
+
+ _updateLogout() {
+ let user = this._userManager.get_user(GLib.get_user_name());
+
+ let allowLogout = !this._lockdownSettings.get_boolean(DISABLE_LOG_OUT_KEY);
+ let alwaysShow = global.settings.get_boolean(ALWAYS_SHOW_LOG_OUT_KEY);
+ let systemAccount = user.system_account;
+ let localAccount = user.local_account;
+ let multiUser = this._userManager.has_multiple_users;
+ let multiSession = Gdm.get_session_ids().length > 1;
+ let shouldShowInMode = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+
+ let visible = allowLogout && (alwaysShow || multiUser || multiSession || systemAccount || !localAccount) && shouldShowInMode;
+ this._actions.get(LOGOUT_ACTION_ID).available = visible;
+ this.notify('can-logout');
+
+ return visible;
+ }
+
+ activateLockOrientation() {
+ if (!this._actions.get(LOCK_ORIENTATION_ACTION_ID).available)
+ throw new Error('The lock-orientation action is not available!');
+
+ let locked = this._orientationSettings.get_boolean('orientation-lock');
+ this._orientationSettings.set_boolean('orientation-lock', !locked);
+ }
+
+ activateLockScreen() {
+ if (!this._actions.get(LOCK_SCREEN_ACTION_ID).available)
+ throw new Error('The lock-screen action is not available!');
+
+ Main.screenShield.lock(true);
+ }
+
+ activateSwitchUser() {
+ if (!this._actions.get(SWITCH_USER_ACTION_ID).available)
+ throw new Error('The switch-user action is not available!');
+
+ if (Main.screenShield)
+ Main.screenShield.lock(false);
+
+ Clutter.threads_add_repaint_func_full(Clutter.RepaintFlags.POST_PAINT, () => {
+ Gdm.goto_login_session_sync(null);
+ return false;
+ });
+ }
+
+ activateLogout() {
+ if (!this._actions.get(LOGOUT_ACTION_ID).available)
+ throw new Error('The logout action is not available!');
+
+ Main.overview.hide();
+ this._session.LogoutRemote(0);
+ }
+
+ activatePowerOff() {
+ if (!this._actions.get(POWER_OFF_ACTION_ID).available)
+ throw new Error('The power-off action is not available!');
+
+ this._session.ShutdownRemote(0);
+ }
+
+ activateRestart() {
+ if (!this._actions.get(RESTART_ACTION_ID).available)
+ throw new Error('The restart action is not available!');
+
+ this._session.RebootRemote();
+ }
+
+ activateSuspend() {
+ if (!this._actions.get(SUSPEND_ACTION_ID).available)
+ throw new Error('The suspend action is not available!');
+
+ this._loginManager.suspend();
+ }
+});
diff --git a/js/misc/util.js b/js/misc/util.js
new file mode 100644
index 0000000..314d766
--- /dev/null
+++ b/js/misc/util.js
@@ -0,0 +1,437 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported findUrls, spawn, spawnCommandLine, spawnApp, trySpawnCommandLine,
+ formatTime, formatTimeSpan, createTimeLabel, insertSorted,
+ ensureActorVisibleInScrollView, wiggle */
+
+const { Clutter, Gio, GLib, Shell, St, GnomeDesktop } = imports.gi;
+const Gettext = imports.gettext;
+
+const Main = imports.ui.main;
+const Params = imports.misc.params;
+
+var SCROLL_TIME = 100;
+
+const WIGGLE_OFFSET = 6;
+const WIGGLE_DURATION = 65;
+const N_WIGGLES = 3;
+
+// http://daringfireball.net/2010/07/improved_regex_for_matching_urls
+const _balancedParens = '\\([^\\s()<>]+\\)';
+const _leadingJunk = '[\\s`(\\[{\'\\"<\u00AB\u201C\u2018]';
+const _notTrailingJunk = '[^\\s`!()\\[\\]{};:\'\\".,<>?\u00AB\u00BB\u200E\u200F\u201C\u201D\u2018\u2019\u202A\u202C]';
+
+const _urlRegexp = new RegExp(
+ '(^|%s)'.format(_leadingJunk) +
+ '(' +
+ '(?:' +
+ '(?:http|https|ftp)://' + // scheme://
+ '|' +
+ 'www\\d{0,3}[.]' + // www.
+ '|' +
+ '[a-z0-9.\\-]+[.][a-z]{2,4}/' + // foo.xx/
+ ')' +
+ '(?:' + // one or more:
+ '[^\\s()<>]+' + // run of non-space non-()
+ '|' + // or
+ '%s'.format(_balancedParens) + // balanced parens
+ ')+' +
+ '(?:' + // end with:
+ '%s'.format(_balancedParens) + // balanced parens
+ '|' + // or
+ '%s'.format(_notTrailingJunk) + // last non-junk char
+ ')' +
+ ')', 'gi');
+
+let _desktopSettings = null;
+
+// findUrls:
+// @str: string to find URLs in
+//
+// Searches @str for URLs and returns an array of objects with %url
+// properties showing the matched URL string, and %pos properties indicating
+// the position within @str where the URL was found.
+//
+// Return value: the list of match objects, as described above
+function findUrls(str) {
+ let res = [], match;
+ while ((match = _urlRegexp.exec(str)))
+ res.push({ url: match[2], pos: match.index + match[1].length });
+ return res;
+}
+
+// spawn:
+// @argv: an argv array
+//
+// Runs @argv in the background, handling any errors that occur
+// when trying to start the program.
+function spawn(argv) {
+ try {
+ trySpawn(argv);
+ } catch (err) {
+ _handleSpawnError(argv[0], err);
+ }
+}
+
+// spawnCommandLine:
+// @commandLine: a command line
+//
+// Runs @commandLine in the background, handling any errors that
+// occur when trying to parse or start the program.
+function spawnCommandLine(commandLine) {
+ try {
+ let [success_, argv] = GLib.shell_parse_argv(commandLine);
+ trySpawn(argv);
+ } catch (err) {
+ _handleSpawnError(commandLine, err);
+ }
+}
+
+// spawnApp:
+// @argv: an argv array
+//
+// Runs @argv as if it was an application, handling startup notification
+function spawnApp(argv) {
+ try {
+ let app = Gio.AppInfo.create_from_commandline(argv.join(' '), null,
+ Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION);
+
+ let context = global.create_app_launch_context(0, -1);
+ app.launch([], context);
+ } catch (err) {
+ _handleSpawnError(argv[0], err);
+ }
+}
+
+// trySpawn:
+// @argv: an argv array
+//
+// Runs @argv in the background. If launching @argv fails,
+// this will throw an error.
+function trySpawn(argv) {
+ var success_, pid;
+ try {
+ [success_, pid] = GLib.spawn_async(null, argv, null,
+ GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD,
+ null);
+ } catch (err) {
+ /* Rewrite the error in case of ENOENT */
+ if (err.matches(GLib.SpawnError, GLib.SpawnError.NOENT)) {
+ throw new GLib.SpawnError({ code: GLib.SpawnError.NOENT,
+ message: _("Command not found") });
+ } else if (err instanceof GLib.Error) {
+ // The exception from gjs contains an error string like:
+ // Error invoking GLib.spawn_command_line_async: Failed to
+ // execute child process "foo" (No such file or directory)
+ // We are only interested in the part in the parentheses. (And
+ // we can't pattern match the text, since it gets localized.)
+ let message = err.message.replace(/.*\((.+)\)/, '$1');
+ throw new err.constructor({ code: err.code, message });
+ } else {
+ throw err;
+ }
+ }
+
+ // Async call, we don't need the reply though
+ GnomeDesktop.start_systemd_scope(argv[0], pid, null, null, null, () => {});
+
+ // Dummy child watch; we don't want to double-fork internally
+ // because then we lose the parent-child relationship, which
+ // can break polkit. See https://bugzilla.redhat.com//show_bug.cgi?id=819275
+ GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid, () => {});
+}
+
+// trySpawnCommandLine:
+// @commandLine: a command line
+//
+// Runs @commandLine in the background. If launching @commandLine
+// fails, this will throw an error.
+function trySpawnCommandLine(commandLine) {
+ let success_, argv;
+
+ try {
+ [success_, argv] = GLib.shell_parse_argv(commandLine);
+ } catch (err) {
+ // Replace "Error invoking GLib.shell_parse_argv: " with
+ // something nicer
+ err.message = err.message.replace(/[^:]*: /, '%s\n'.format(_('Could not parse command:')));
+ throw err;
+ }
+
+ trySpawn(argv);
+}
+
+function _handleSpawnError(command, err) {
+ let title = _("Execution of ā€œ%sā€ failed:").format(command);
+ Main.notifyError(title, err.message);
+}
+
+function formatTimeSpan(date) {
+ let now = GLib.DateTime.new_now_local();
+
+ let timespan = now.difference(date);
+
+ let minutesAgo = timespan / GLib.TIME_SPAN_MINUTE;
+ let hoursAgo = timespan / GLib.TIME_SPAN_HOUR;
+ let daysAgo = timespan / GLib.TIME_SPAN_DAY;
+ let weeksAgo = daysAgo / 7;
+ let monthsAgo = daysAgo / 30;
+ let yearsAgo = weeksAgo / 52;
+
+ if (minutesAgo < 5)
+ return _("Just now");
+ if (hoursAgo < 1) {
+ return Gettext.ngettext("%d minute ago",
+ "%d minutes ago", minutesAgo).format(minutesAgo);
+ }
+ if (daysAgo < 1) {
+ return Gettext.ngettext("%d hour ago",
+ "%d hours ago", hoursAgo).format(hoursAgo);
+ }
+ if (daysAgo < 2)
+ return _("Yesterday");
+ if (daysAgo < 15) {
+ return Gettext.ngettext("%d day ago",
+ "%d days ago", daysAgo).format(daysAgo);
+ }
+ if (weeksAgo < 8) {
+ return Gettext.ngettext("%d week ago",
+ "%d weeks ago", weeksAgo).format(weeksAgo);
+ }
+ if (yearsAgo < 1) {
+ return Gettext.ngettext("%d month ago",
+ "%d months ago", monthsAgo).format(monthsAgo);
+ }
+ return Gettext.ngettext("%d year ago",
+ "%d years ago", yearsAgo).format(yearsAgo);
+}
+
+function formatTime(time, params) {
+ let date;
+ // HACK: The built-in Date type sucks at timezones, which we need for the
+ // world clock; it's often more convenient though, so allow either
+ // Date or GLib.DateTime as parameter
+ if (time instanceof Date)
+ date = GLib.DateTime.new_from_unix_local(time.getTime() / 1000);
+ else
+ date = time;
+
+ let now = GLib.DateTime.new_now_local();
+
+ let daysAgo = now.difference(date) / (24 * 60 * 60 * 1000 * 1000);
+
+ let format;
+
+ if (_desktopSettings == null)
+ _desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
+ let clockFormat = _desktopSettings.get_string('clock-format');
+
+ params = Params.parse(params, {
+ timeOnly: false,
+ ampm: true,
+ });
+
+ if (clockFormat == '24h') {
+ // Show only the time if date is on today
+ if (daysAgo < 1 || params.timeOnly)
+ /* Translators: Time in 24h format */
+ format = N_("%H\u2236%M");
+ // Show the word "Yesterday" and time if date is on yesterday
+ else if (daysAgo < 2)
+ /* Translators: this is the word "Yesterday" followed by a
+ time string in 24h format. i.e. "Yesterday, 14:30" */
+ // xgettext:no-c-format
+ format = N_("Yesterday, %H\u2236%M");
+ // Show a week day and time if date is in the last week
+ else if (daysAgo < 7)
+ /* Translators: this is the week day name followed by a time
+ string in 24h format. i.e. "Monday, 14:30" */
+ // xgettext:no-c-format
+ format = N_("%A, %H\u2236%M");
+ else if (date.get_year() == now.get_year())
+ /* Translators: this is the month name and day number
+ followed by a time string in 24h format.
+ i.e. "May 25, 14:30" */
+ // xgettext:no-c-format
+ format = N_("%B %-d, %H\u2236%M");
+ else
+ /* Translators: this is the month name, day number, year
+ number followed by a time string in 24h format.
+ i.e. "May 25 2012, 14:30" */
+ // xgettext:no-c-format
+ format = N_("%B %-d %Y, %H\u2236%M");
+ } else {
+ // Show only the time if date is on today
+ if (daysAgo < 1 || params.timeOnly) // eslint-disable-line no-lonely-if
+ /* Translators: Time in 12h format */
+ format = N_("%l\u2236%M %p");
+ // Show the word "Yesterday" and time if date is on yesterday
+ else if (daysAgo < 2)
+ /* Translators: this is the word "Yesterday" followed by a
+ time string in 12h format. i.e. "Yesterday, 2:30 pm" */
+ // xgettext:no-c-format
+ format = N_("Yesterday, %l\u2236%M %p");
+ // Show a week day and time if date is in the last week
+ else if (daysAgo < 7)
+ /* Translators: this is the week day name followed by a time
+ string in 12h format. i.e. "Monday, 2:30 pm" */
+ // xgettext:no-c-format
+ format = N_("%A, %l\u2236%M %p");
+ else if (date.get_year() == now.get_year())
+ /* Translators: this is the month name and day number
+ followed by a time string in 12h format.
+ i.e. "May 25, 2:30 pm" */
+ // xgettext:no-c-format
+ format = N_("%B %-d, %l\u2236%M %p");
+ else
+ /* Translators: this is the month name, day number, year
+ number followed by a time string in 12h format.
+ i.e. "May 25 2012, 2:30 pm"*/
+ // xgettext:no-c-format
+ format = N_("%B %-d %Y, %l\u2236%M %p");
+ }
+
+ // Time in short 12h format, without the equivalent of "AM" or "PM"; used
+ // when it is clear from the context
+ if (!params.ampm)
+ format = format.replace(/\s*%p/g, '');
+
+ let formattedTime = date.format(Shell.util_translate_time_string(format));
+ // prepend LTR-mark to colon/ratio to force a text direction on times
+ return formattedTime.replace(/([:\u2236])/g, '\u200e$1');
+}
+
+function createTimeLabel(date, params) {
+ if (_desktopSettings == null)
+ _desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
+
+ let label = new St.Label({ text: formatTime(date, params) });
+ let id = _desktopSettings.connect('changed::clock-format', () => {
+ label.text = formatTime(date, params);
+ });
+ label.connect('destroy', () => _desktopSettings.disconnect(id));
+ return label;
+}
+
+// lowerBound:
+// @array: an array or array-like object, already sorted
+// according to @cmp
+// @val: the value to add
+// @cmp: a comparator (or undefined to compare as numbers)
+//
+// Returns the position of the first element that is not
+// lower than @val, according to @cmp.
+// That is, returns the first position at which it
+// is possible to insert @val without violating the
+// order.
+// This is quite like an ordinary binary search, except
+// that it doesn't stop at first element comparing equal.
+
+function lowerBound(array, val, cmp) {
+ let min, max, mid, v;
+ cmp = cmp || ((a, b) => a - b);
+
+ if (array.length == 0)
+ return 0;
+
+ min = 0;
+ max = array.length;
+ while (min < (max - 1)) {
+ mid = Math.floor((min + max) / 2);
+ v = cmp(array[mid], val);
+
+ if (v < 0)
+ min = mid + 1;
+ else
+ max = mid;
+ }
+
+ return min == max || cmp(array[min], val) < 0 ? max : min;
+}
+
+// insertSorted:
+// @array: an array sorted according to @cmp
+// @val: a value to insert
+// @cmp: the sorting function
+//
+// Inserts @val into @array, preserving the
+// sorting invariants.
+// Returns the position at which it was inserted
+function insertSorted(array, val, cmp) {
+ let pos = lowerBound(array, val, cmp);
+ array.splice(pos, 0, val);
+
+ return pos;
+}
+
+function ensureActorVisibleInScrollView(scrollView, actor) {
+ let adjustment = scrollView.vscroll.adjustment;
+ let [value, lower_, upper, stepIncrement_, pageIncrement_, pageSize] = adjustment.get_values();
+
+ let offset = 0;
+ let vfade = scrollView.get_effect("fade");
+ if (vfade)
+ offset = vfade.vfade_offset;
+
+ let box = actor.get_allocation_box();
+ let y1 = box.y1, y2 = box.y2;
+
+ let parent = actor.get_parent();
+ while (parent != scrollView) {
+ if (!parent)
+ throw new Error("actor not in scroll view");
+
+ box = parent.get_allocation_box();
+ y1 += box.y1;
+ y2 += box.y1;
+ parent = parent.get_parent();
+ }
+
+ if (y1 < value + offset)
+ value = Math.max(0, y1 - offset);
+ else if (y2 > value + pageSize - offset)
+ value = Math.min(upper, y2 + offset - pageSize);
+ else
+ return;
+
+ adjustment.ease(value, {
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ duration: SCROLL_TIME,
+ });
+}
+
+function wiggle(actor, params) {
+ if (!St.Settings.get().enable_animations)
+ return;
+
+ params = Params.parse(params, {
+ offset: WIGGLE_OFFSET,
+ duration: WIGGLE_DURATION,
+ wiggleCount: N_WIGGLES,
+ });
+ actor.translation_x = 0;
+
+ // Accelerate before wiggling
+ actor.ease({
+ translation_x: -params.offset,
+ duration: params.duration,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ // Wiggle
+ actor.ease({
+ translation_x: params.offset,
+ duration: params.duration,
+ mode: Clutter.AnimationMode.LINEAR,
+ repeatCount: params.wiggleCount,
+ autoReverse: true,
+ onComplete: () => {
+ // Decelerate and return to the original position
+ actor.ease({
+ translation_x: 0,
+ duration: params.duration,
+ mode: Clutter.AnimationMode.EASE_IN_QUAD,
+ });
+ },
+ });
+ },
+ });
+}
diff --git a/js/misc/weather.js b/js/misc/weather.js
new file mode 100644
index 0000000..e518f22
--- /dev/null
+++ b/js/misc/weather.js
@@ -0,0 +1,320 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const { Geoclue, Gio, GLib, GWeather, Shell } = imports.gi;
+const Signals = imports.signals;
+
+const PermissionStore = imports.misc.permissionStore;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+Gio._promisify(Geoclue.Simple, 'new', 'new_finish');
+
+const WeatherIntegrationIface = loadInterfaceXML('org.gnome.Shell.WeatherIntegration');
+
+const WEATHER_BUS_NAME = 'org.gnome.Weather';
+const WEATHER_OBJECT_PATH = '/org/gnome/Weather';
+const WEATHER_INTEGRATION_IFACE = 'org.gnome.Shell.WeatherIntegration';
+
+const WEATHER_APP_ID = 'org.gnome.Weather.desktop';
+
+// Minimum time between updates to show loading indication
+var UPDATE_THRESHOLD = 10 * GLib.TIME_SPAN_MINUTE;
+
+var WeatherClient = class {
+ constructor() {
+ this._loading = false;
+ this._locationValid = false;
+ this._lastUpdate = GLib.DateTime.new_from_unix_local(0);
+
+ this._autoLocationRequested = false;
+ this._mostRecentLocation = null;
+
+ this._gclueService = null;
+ this._gclueStarted = false;
+ this._gclueStarting = false;
+ this._gclueLocationChangedId = 0;
+
+ this._needsAuth = true;
+ this._weatherAuthorized = false;
+ this._permStore = new PermissionStore.PermissionStore((proxy, error) => {
+ if (error) {
+ log(`Failed to connect to permissionStore: ${error.message}`);
+ return;
+ }
+
+ if (this._permStore.g_name_owner == null) {
+ // Failed to auto-start, likely because xdg-desktop-portal
+ // isn't installed; don't restrict access to location service
+ this._weatherAuthorized = true;
+ this._updateAutoLocation();
+ return;
+ }
+
+ this._permStore.LookupRemote('gnome', 'geolocation', (res, err) => {
+ if (err)
+ log(`Error looking up permission: ${err.message}`);
+
+ let [perms, data] = err ? [{}, null] : res;
+ let params = ['gnome', 'geolocation', false, data, perms];
+ this._onPermStoreChanged(this._permStore, '', params);
+ });
+ });
+ this._permStore.connectSignal('Changed',
+ this._onPermStoreChanged.bind(this));
+
+ this._locationSettings = new Gio.Settings({ schema_id: 'org.gnome.system.location' });
+ this._locationSettings.connect('changed::enabled',
+ this._updateAutoLocation.bind(this));
+
+ this._world = GWeather.Location.get_world();
+
+ this._providers = GWeather.Provider.METAR |
+ GWeather.Provider.YR_NO |
+ GWeather.Provider.OWM;
+
+ this._weatherInfo = new GWeather.Info({ enabled_providers: 0 });
+ this._weatherInfo.connect_after('updated', () => {
+ this._lastUpdate = GLib.DateTime.new_now_local();
+ this.emit('changed');
+ });
+
+ this._weatherApp = null;
+ this._weatherProxy = null;
+
+ this._createWeatherProxy();
+
+ this._settings = new Gio.Settings({
+ schema_id: 'org.gnome.shell.weather',
+ });
+ this._settings.connect('changed::automatic-location',
+ this._onAutomaticLocationChanged.bind(this));
+ this._onAutomaticLocationChanged();
+ this._settings.connect('changed::locations',
+ this._onLocationsChanged.bind(this));
+ this._onLocationsChanged();
+
+ this._appSystem = Shell.AppSystem.get_default();
+ this._appSystem.connect('installed-changed',
+ this._onInstalledChanged.bind(this));
+ this._onInstalledChanged();
+ }
+
+ get available() {
+ return this._weatherApp != null;
+ }
+
+ get loading() {
+ return this._loading;
+ }
+
+ get hasLocation() {
+ return this._locationValid;
+ }
+
+ get info() {
+ return this._weatherInfo;
+ }
+
+ activateApp() {
+ if (this._weatherApp)
+ this._weatherApp.activate();
+ }
+
+ update() {
+ if (!this._locationValid)
+ return;
+
+ let now = GLib.DateTime.new_now_local();
+ // Update without loading indication if the current info is recent enough
+ if (this._weatherInfo.is_valid() &&
+ now.difference(this._lastUpdate) < UPDATE_THRESHOLD)
+ this._weatherInfo.update();
+ else
+ this._loadInfo();
+ }
+
+ get _useAutoLocation() {
+ return this._autoLocationRequested &&
+ this._locationSettings.get_boolean('enabled') &&
+ (!this._needsAuth || this._weatherAuthorized);
+ }
+
+ async _createWeatherProxy() {
+ const nodeInfo = Gio.DBusNodeInfo.new_for_xml(WeatherIntegrationIface);
+ try {
+ this._weatherProxy = await Gio.DBusProxy.new(
+ Gio.DBus.session,
+ Gio.DBusProxyFlags.DO_NOT_AUTO_START | Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES,
+ nodeInfo.lookup_interface(WEATHER_INTEGRATION_IFACE),
+ WEATHER_BUS_NAME,
+ WEATHER_OBJECT_PATH,
+ WEATHER_INTEGRATION_IFACE,
+ null);
+ } catch (e) {
+ log(`Failed to create GNOME Weather proxy: ${e}`);
+ return;
+ }
+
+ this._weatherProxy.connect('g-properties-changed',
+ this._onWeatherPropertiesChanged.bind(this));
+ this._onWeatherPropertiesChanged();
+ }
+
+ _onWeatherPropertiesChanged() {
+ if (this._weatherProxy.g_name_owner == null)
+ return;
+
+ this._settings.set_boolean('automatic-location',
+ this._weatherProxy.AutomaticLocation);
+ this._settings.set_value('locations',
+ new GLib.Variant('av', this._weatherProxy.Locations));
+ }
+
+ _onInstalledChanged() {
+ let hadApp = this._weatherApp != null;
+ this._weatherApp = this._appSystem.lookup_app(WEATHER_APP_ID);
+ let haveApp = this._weatherApp != null;
+
+ if (hadApp !== haveApp)
+ this.emit('changed');
+
+ let neededAuth = this._needsAuth;
+ this._needsAuth = this._weatherApp === null ||
+ this._weatherApp.app_info.has_key('X-Flatpak');
+
+ if (neededAuth !== this._needsAuth)
+ this._updateAutoLocation();
+ }
+
+ _loadInfo() {
+ let id = this._weatherInfo.connect('updated', () => {
+ this._weatherInfo.disconnect(id);
+ this._loading = false;
+ });
+
+ this._loading = true;
+ this.emit('changed');
+
+ this._weatherInfo.update();
+ }
+
+ _locationsEqual(loc1, loc2) {
+ if (loc1 == loc2)
+ return true;
+
+ if (loc1 == null || loc2 == null)
+ return false;
+
+ return loc1.equal(loc2);
+ }
+
+ _setLocation(location) {
+ if (this._locationsEqual(this._weatherInfo.location, location))
+ return;
+
+ this._weatherInfo.abort();
+ this._weatherInfo.set_location(location);
+ this._locationValid = location != null;
+
+ this._weatherInfo.set_enabled_providers(location ? this._providers : 0);
+
+ if (location)
+ this._loadInfo();
+ else
+ this.emit('changed');
+ }
+
+ _updateLocationMonitoring() {
+ if (this._useAutoLocation) {
+ if (this._gclueLocationChangedId != 0 || this._gclueService == null)
+ return;
+
+ this._gclueLocationChangedId =
+ this._gclueService.connect('notify::location',
+ this._onGClueLocationChanged.bind(this));
+ this._onGClueLocationChanged();
+ } else {
+ if (this._gclueLocationChangedId)
+ this._gclueService.disconnect(this._gclueLocationChangedId);
+ this._gclueLocationChangedId = 0;
+ }
+ }
+
+ async _startGClueService() {
+ if (this._gclueStarting)
+ return;
+
+ this._gclueStarting = true;
+
+ try {
+ this._gclueService = await Geoclue.Simple.new(
+ 'org.gnome.Shell', Geoclue.AccuracyLevel.CITY, null);
+ } catch (e) {
+ log(`Failed to connect to Geoclue2 service: ${e.message}`);
+ this._setLocation(this._mostRecentLocation);
+ return;
+ }
+ this._gclueStarted = true;
+ this._gclueService.get_client().distance_threshold = 100;
+ this._updateLocationMonitoring();
+ }
+
+ _onGClueLocationChanged() {
+ let geoLocation = this._gclueService.location;
+ let location = GWeather.Location.new_detached(geoLocation.description,
+ null,
+ geoLocation.latitude,
+ geoLocation.longitude);
+ this._setLocation(location);
+ }
+
+ _onAutomaticLocationChanged() {
+ let useAutoLocation = this._settings.get_boolean('automatic-location');
+ if (this._autoLocationRequested == useAutoLocation)
+ return;
+
+ this._autoLocationRequested = useAutoLocation;
+
+ this._updateAutoLocation();
+ }
+
+ _updateAutoLocation() {
+ this._updateLocationMonitoring();
+
+ if (this._useAutoLocation)
+ this._startGClueService();
+ else
+ this._setLocation(this._mostRecentLocation);
+ }
+
+ _onLocationsChanged() {
+ let locations = this._settings.get_value('locations').deep_unpack();
+ let serialized = locations.shift();
+ let mostRecentLocation = null;
+
+ if (serialized)
+ mostRecentLocation = this._world.deserialize(serialized);
+
+ if (this._locationsEqual(this._mostRecentLocation, mostRecentLocation))
+ return;
+
+ this._mostRecentLocation = mostRecentLocation;
+
+ if (!this._useAutoLocation || !this._gclueStarted)
+ this._setLocation(this._mostRecentLocation);
+ }
+
+ _onPermStoreChanged(proxy, sender, params) {
+ let [table, id, deleted_, data_, perms] = params;
+
+ if (table != 'gnome' || id != 'geolocation')
+ return;
+
+ let permission = perms['org.gnome.Weather'] || ['NONE'];
+ let [accuracy] = permission;
+ this._weatherAuthorized = accuracy != 'NONE';
+
+ this._updateAutoLocation();
+ }
+};
+Signals.addSignalMethods(WeatherClient.prototype);