9928125e7d
By direclty using the underlying GSetting, whether or not an extension appears as enabled or disabled currently depends only on whether it is included in the 'enabled-extensions' list or not. However this doesn't necessarily reflect the real extension state, as an extension may be in error state, or enabled via the session mode. Switch to the extensions D-Bus API to ensure that the list of extensions and each extension's state correctly reflects the state in gnome-shell. https://bugzilla.gnome.org/show_bug.cgi?id=789852
698 lines
21 KiB
JavaScript
698 lines
21 KiB
JavaScript
const Gettext = imports.gettext;
|
||
const { Gdk, GLib, Gio, GObject, Gtk, Pango } = imports.gi;
|
||
const Format = imports.format;
|
||
|
||
const _ = Gettext.gettext;
|
||
|
||
const Config = imports.misc.config;
|
||
const ExtensionUtils = imports.misc.extensionUtils;
|
||
const { loadInterfaceXML } = imports.misc.fileUtils;
|
||
|
||
const { ExtensionState } = ExtensionUtils;
|
||
|
||
const GnomeShellIface = loadInterfaceXML('org.gnome.Shell.Extensions');
|
||
const GnomeShellProxy = Gio.DBusProxy.makeProxyWrapper(GnomeShellIface);
|
||
|
||
function stripPrefix(string, prefix) {
|
||
if (string.slice(0, prefix.length) == prefix)
|
||
return string.slice(prefix.length);
|
||
return string;
|
||
}
|
||
|
||
var Application = GObject.registerClass({
|
||
GTypeName: 'ExtensionPrefs_Application'
|
||
}, class Application extends Gtk.Application {
|
||
_init() {
|
||
GLib.set_prgname('gnome-shell-extension-prefs');
|
||
super._init({
|
||
application_id: 'org.gnome.shell.ExtensionPrefs',
|
||
flags: Gio.ApplicationFlags.HANDLES_COMMAND_LINE
|
||
});
|
||
|
||
this._startupUuid = null;
|
||
this._loaded = false;
|
||
this._skipMainWindow = false;
|
||
this._shellProxy = null;
|
||
}
|
||
|
||
get shellProxy() {
|
||
return this._shellProxy;
|
||
}
|
||
|
||
_showPrefs(uuid) {
|
||
let row = this._extensionSelector.get_children().find(c => {
|
||
return c.uuid === uuid && c.hasPrefs;
|
||
});
|
||
|
||
if (!row)
|
||
return false;
|
||
|
||
let widget;
|
||
|
||
try {
|
||
widget = row.prefsModule.buildPrefsWidget();
|
||
} catch (e) {
|
||
widget = this._buildErrorUI(row, e);
|
||
}
|
||
|
||
let dialog = new Gtk.Window({
|
||
modal: !this._skipMainWindow,
|
||
type_hint: Gdk.WindowTypeHint.DIALOG
|
||
});
|
||
dialog.set_titlebar(new Gtk.HeaderBar({
|
||
show_close_button: true,
|
||
title: row.name,
|
||
visible: true
|
||
}));
|
||
|
||
if (this._skipMainWindow) {
|
||
this.add_window(dialog);
|
||
if (this._window)
|
||
this._window.destroy();
|
||
this._window = dialog;
|
||
this._window.window_position = Gtk.WindowPosition.CENTER;
|
||
} else {
|
||
dialog.transient_for = this._window;
|
||
}
|
||
|
||
dialog.set_default_size(600, 400);
|
||
dialog.add(widget);
|
||
dialog.show();
|
||
}
|
||
|
||
_buildErrorUI(row, exc) {
|
||
let scroll = new Gtk.ScrolledWindow({
|
||
hscrollbar_policy: Gtk.PolicyType.NEVER,
|
||
propagate_natural_height: true
|
||
});
|
||
|
||
let box = new Gtk.Box({
|
||
orientation: Gtk.Orientation.VERTICAL,
|
||
spacing: 12,
|
||
margin: 100,
|
||
margin_bottom: 60
|
||
});
|
||
scroll.add(box);
|
||
|
||
let label = new Gtk.Label({
|
||
label: '<span size="x-large">%s</span>'.format(_("Something’s gone wrong")),
|
||
use_markup: true
|
||
});
|
||
label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
|
||
box.add(label);
|
||
|
||
label = new Gtk.Label({
|
||
label: _("We’re very sorry, but there’s been a problem: the settings for this extension can’t be displayed. We recommend that you report the issue to the extension authors."),
|
||
justify: Gtk.Justification.CENTER,
|
||
wrap: true
|
||
});
|
||
box.add(label);
|
||
|
||
let expander = new Expander({
|
||
label: _("Technical Details"),
|
||
margin_top: 12
|
||
});
|
||
box.add(expander);
|
||
|
||
let errortext = `${exc}\n\nStack trace:\n${
|
||
// Indent stack trace.
|
||
exc.stack.split('\n').map(line => ` ${line}`).join('\n')
|
||
}`;
|
||
|
||
let buffer = new Gtk.TextBuffer({ text: errortext });
|
||
let textview = new Gtk.TextView({
|
||
buffer: buffer,
|
||
wrap_mode: Gtk.WrapMode.WORD,
|
||
monospace: true,
|
||
editable: false,
|
||
top_margin: 12,
|
||
bottom_margin: 12,
|
||
left_margin: 12,
|
||
right_margin: 12
|
||
});
|
||
|
||
let toolbar = new Gtk.Toolbar();
|
||
let provider = new Gtk.CssProvider();
|
||
provider.load_from_data(`* {
|
||
border: 0 solid @borders;
|
||
border-top-width: 1px;
|
||
}`);
|
||
toolbar.get_style_context().add_provider(
|
||
provider,
|
||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||
);
|
||
|
||
let copyButton = new Gtk.ToolButton({
|
||
icon_name: 'edit-copy-symbolic',
|
||
tooltip_text: _("Copy Error")
|
||
});
|
||
toolbar.add(copyButton);
|
||
|
||
copyButton.connect('clicked', w => {
|
||
let clipboard = Gtk.Clipboard.get_default(w.get_display());
|
||
// markdown for pasting in gitlab issues
|
||
let lines = [
|
||
`The settings of extension ${row.uuid} had an error:`,
|
||
'```',
|
||
`${exc}`,
|
||
'```',
|
||
'',
|
||
'Stack trace:',
|
||
'```',
|
||
exc.stack.replace(/\n$/, ''), // stack without trailing newline
|
||
'```',
|
||
''
|
||
];
|
||
clipboard.set_text(lines.join('\n'), -1);
|
||
});
|
||
|
||
let spacing = new Gtk.SeparatorToolItem({ draw: false });
|
||
toolbar.add(spacing);
|
||
toolbar.child_set_property(spacing, "expand", true);
|
||
|
||
let urlButton = new Gtk.ToolButton({
|
||
label: _("Homepage"),
|
||
tooltip_text: _("Visit extension homepage"),
|
||
no_show_all: true,
|
||
visible: row.url != null
|
||
});
|
||
toolbar.add(urlButton);
|
||
|
||
urlButton.connect('clicked', w => {
|
||
let context = w.get_display().get_app_launch_context();
|
||
Gio.AppInfo.launch_default_for_uri(row.url, context);
|
||
});
|
||
|
||
let expandedBox = new Gtk.Box({
|
||
orientation: Gtk.Orientation.VERTICAL
|
||
});
|
||
expandedBox.add(textview);
|
||
expandedBox.add(toolbar);
|
||
|
||
expander.add(expandedBox);
|
||
|
||
scroll.show_all();
|
||
return scroll;
|
||
}
|
||
|
||
_buildUI() {
|
||
this._window = new Gtk.ApplicationWindow({ application: this,
|
||
window_position: Gtk.WindowPosition.CENTER });
|
||
|
||
this._window.set_default_size(800, 500);
|
||
|
||
this._titlebar = new Gtk.HeaderBar({ show_close_button: true,
|
||
title: _("Shell Extensions") });
|
||
this._window.set_titlebar(this._titlebar);
|
||
|
||
let killSwitch = new Gtk.Switch({ valign: Gtk.Align.CENTER });
|
||
this._titlebar.pack_end(killSwitch);
|
||
|
||
this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell' });
|
||
this._settings.bind('disable-user-extensions', killSwitch, 'active',
|
||
Gio.SettingsBindFlags.DEFAULT |
|
||
Gio.SettingsBindFlags.INVERT_BOOLEAN);
|
||
|
||
this._mainStack = new Gtk.Stack({
|
||
transition_type: Gtk.StackTransitionType.CROSSFADE
|
||
});
|
||
this._window.add(this._mainStack);
|
||
|
||
let scroll = new Gtk.ScrolledWindow({ hscrollbar_policy: Gtk.PolicyType.NEVER });
|
||
|
||
this._extensionSelector = new Gtk.ListBox({ selection_mode: Gtk.SelectionMode.NONE });
|
||
this._extensionSelector.set_sort_func(this._sortList.bind(this));
|
||
this._extensionSelector.set_header_func(this._updateHeader.bind(this));
|
||
|
||
scroll.add(this._extensionSelector);
|
||
|
||
this._mainStack.add_named(scroll, 'listing');
|
||
this._mainStack.add_named(new EmptyPlaceholder(), 'placeholder');
|
||
|
||
this._shellProxy = new GnomeShellProxy(Gio.DBus.session, 'org.gnome.Shell', '/org/gnome/Shell');
|
||
this._shellProxy.connectSignal('ExtensionStateChanged',
|
||
this._onExtensionStateChanged.bind(this));
|
||
|
||
this._window.show_all();
|
||
}
|
||
|
||
_sortList(row1, row2) {
|
||
return row1.name.localeCompare(row2.name);
|
||
}
|
||
|
||
_updateHeader(row, before) {
|
||
if (!before || row.get_header())
|
||
return;
|
||
|
||
let sep = new Gtk.Separator({ orientation: Gtk.Orientation.HORIZONTAL });
|
||
row.set_header(sep);
|
||
}
|
||
|
||
_findExtensionRow(uuid) {
|
||
return this._extensionSelector.get_children().find(c => c.uuid === uuid);
|
||
}
|
||
|
||
_onExtensionStateChanged(proxy, senderName, [uuid, newState]) {
|
||
let row = this._findExtensionRow(uuid);
|
||
if (row) {
|
||
let { state } = ExtensionUtils.deserializeExtension(newState);
|
||
if (state == ExtensionState.UNINSTALLED)
|
||
row.destroy();
|
||
return; // we only deal with new and deleted extensions here
|
||
}
|
||
|
||
this._shellProxy.GetExtensionInfoRemote(uuid, ([serialized]) => {
|
||
let extension = ExtensionUtils.deserializeExtension(serialized);
|
||
if (!extension)
|
||
return;
|
||
// check the extension wasn't added in between
|
||
if (this._findExtensionRow(uuid) != null)
|
||
return;
|
||
this._addExtensionRow(extension);
|
||
});
|
||
}
|
||
|
||
_scanExtensions() {
|
||
this._shellProxy.ListExtensionsRemote(([extensionsMap], e) => {
|
||
if (e) {
|
||
if (e instanceof Gio.DBusError) {
|
||
log(`Failed to connect to shell proxy: ${e}`);
|
||
this._mainStack.add_named(new NoShellPlaceholder(), 'noshell');
|
||
this._mainStack.visible_child_name = 'noshell';
|
||
} else
|
||
throw e;
|
||
return;
|
||
}
|
||
|
||
for (let uuid in extensionsMap) {
|
||
let extension = ExtensionUtils.deserializeExtension(extensionsMap[uuid]);
|
||
this._addExtensionRow(extension);
|
||
}
|
||
this._extensionsLoaded();
|
||
});
|
||
}
|
||
|
||
_addExtensionRow(extension) {
|
||
let row = new ExtensionRow(extension);
|
||
|
||
row.prefsButton.connect('clicked', () => {
|
||
this._showPrefs(row.uuid);
|
||
});
|
||
|
||
row.show_all();
|
||
this._extensionSelector.add(row);
|
||
}
|
||
|
||
_extensionsLoaded() {
|
||
if (this._extensionSelector.get_children().length > 0)
|
||
this._mainStack.visible_child_name = 'listing';
|
||
else
|
||
this._mainStack.visible_child_name = 'placeholder';
|
||
|
||
if (this._startupUuid)
|
||
this._showPrefs(this._startupUuid);
|
||
this._startupUuid = null;
|
||
this._skipMainWindow = false;
|
||
this._loaded = true;
|
||
}
|
||
|
||
vfunc_activate() {
|
||
this._window.present();
|
||
}
|
||
|
||
vfunc_startup() {
|
||
super.vfunc_startup();
|
||
|
||
this._buildUI();
|
||
this._scanExtensions();
|
||
}
|
||
|
||
vfunc_command_line(commandLine) {
|
||
this.activate();
|
||
let args = commandLine.get_arguments();
|
||
|
||
if (args.length) {
|
||
let uuid = args[0];
|
||
|
||
this._skipMainWindow = true;
|
||
|
||
// Strip off "extension:///" prefix which fakes a URI, if it exists
|
||
uuid = stripPrefix(uuid, "extension:///");
|
||
|
||
if (!this._loaded)
|
||
this._startupUuid = uuid;
|
||
else if (!this._showPrefs(uuid))
|
||
this._skipMainWindow = false;
|
||
}
|
||
return 0;
|
||
}
|
||
});
|
||
|
||
var Expander = GObject.registerClass({
|
||
Properties: {
|
||
'label': GObject.ParamSpec.string(
|
||
'label', 'label', 'label',
|
||
GObject.ParamFlags.READWRITE,
|
||
null
|
||
)
|
||
}
|
||
}, class Expander extends Gtk.Box {
|
||
_init(params = {}) {
|
||
this._labelText = null;
|
||
|
||
super._init(Object.assign(params, {
|
||
orientation: Gtk.Orientation.VERTICAL,
|
||
spacing: 0
|
||
}));
|
||
|
||
this._frame = new Gtk.Frame({
|
||
shadow_type: Gtk.ShadowType.IN,
|
||
hexpand: true
|
||
});
|
||
|
||
let eventBox = new Gtk.EventBox();
|
||
this._frame.add(eventBox);
|
||
|
||
let hbox = new Gtk.Box({
|
||
spacing: 6,
|
||
margin: 12
|
||
});
|
||
eventBox.add(hbox);
|
||
|
||
this._arrow = new Gtk.Image({
|
||
icon_name: 'pan-end-symbolic'
|
||
});
|
||
hbox.add(this._arrow);
|
||
|
||
this._label = new Gtk.Label({ label: this._labelText });
|
||
hbox.add(this._label);
|
||
|
||
this._revealer = new Gtk.Revealer();
|
||
|
||
this._childBin = new Gtk.Frame({
|
||
shadow_type: Gtk.ShadowType.IN
|
||
});
|
||
this._revealer.add(this._childBin);
|
||
|
||
// Directly chain up to parent for internal children
|
||
super.add(this._frame);
|
||
super.add(this._revealer);
|
||
|
||
let provider = new Gtk.CssProvider();
|
||
provider.load_from_data('* { border-top-width: 0; }');
|
||
this._childBin.get_style_context().add_provider(
|
||
provider,
|
||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||
);
|
||
|
||
this._gesture = new Gtk.GestureMultiPress({
|
||
widget: this._frame,
|
||
button: 0,
|
||
exclusive: true
|
||
});
|
||
this._gesture.connect('released', (gesture, nPress) => {
|
||
if (nPress == 1)
|
||
this._revealer.reveal_child = !this._revealer.reveal_child;
|
||
});
|
||
this._revealer.connect('notify::reveal-child', () => {
|
||
if (this._revealer.reveal_child)
|
||
this._arrow.icon_name = 'pan-down-symbolic';
|
||
else
|
||
this._arrow.icon_name = 'pan-end-symbolic';
|
||
});
|
||
}
|
||
|
||
get label() {
|
||
return this._labelText;
|
||
}
|
||
|
||
set label(text) {
|
||
if (this._labelText == text)
|
||
return;
|
||
|
||
if (this._label)
|
||
this._label.label = text;
|
||
this._labelText = text;
|
||
this.notify('label');
|
||
}
|
||
|
||
add(child) {
|
||
// set expanded child
|
||
this._childBin.get_children().forEach(c => {
|
||
this._childBin.remove(c);
|
||
});
|
||
|
||
if (child)
|
||
this._childBin.add(child);
|
||
}
|
||
});
|
||
|
||
var EmptyPlaceholder = GObject.registerClass(
|
||
class EmptyPlaceholder extends Gtk.Box {
|
||
_init() {
|
||
super._init({
|
||
orientation: Gtk.Orientation.VERTICAL,
|
||
spacing: 6,
|
||
margin: 32
|
||
});
|
||
|
||
let image = new Gtk.Image({
|
||
icon_name: 'application-x-addon-symbolic',
|
||
pixel_size: 96,
|
||
visible: true,
|
||
vexpand: true,
|
||
valign: Gtk.Align.END
|
||
});
|
||
image.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
|
||
this.add(image);
|
||
|
||
let label = new Gtk.Label({
|
||
label: `<b><span size="x-large">${_("No Extensions Installed" )}</span></b>`,
|
||
use_markup: true,
|
||
visible: true
|
||
});
|
||
label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
|
||
this.add(label);
|
||
|
||
let appInfo = Gio.DesktopAppInfo.new('org.gnome.Software.desktop');
|
||
|
||
let desc = new Gtk.Label({
|
||
label: _("Extensions can be installed through Software or <a href=\"https://extensions.gnome.org\">extensions.gnome.org</a>."),
|
||
use_markup: true,
|
||
wrap: true,
|
||
justify: Gtk.Justification.CENTER,
|
||
visible: true,
|
||
max_width_chars: 50,
|
||
hexpand: true,
|
||
vexpand: (appInfo == null),
|
||
halign: Gtk.Align.CENTER,
|
||
valign: Gtk.Align.START
|
||
});
|
||
this.add(desc);
|
||
|
||
if (appInfo) {
|
||
let button = new Gtk.Button({
|
||
label: _("Browse in Software"),
|
||
image: new Gtk.Image({
|
||
icon_name: "org.gnome.Software-symbolic"
|
||
}),
|
||
always_show_image: true,
|
||
margin_top: 12,
|
||
visible: true,
|
||
halign: Gtk.Align.CENTER,
|
||
valign: Gtk.Align.START,
|
||
vexpand: true
|
||
});
|
||
this.add(button);
|
||
|
||
button.connect('clicked', w => {
|
||
let context = w.get_display().get_app_launch_context();
|
||
appInfo.launch([], context);
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
var NoShellPlaceholder = GObject.registerClass(
|
||
class NoShellPlaceholder extends Gtk.Box {
|
||
_init() {
|
||
super._init({
|
||
orientation: Gtk.Orientation.VERTICAL,
|
||
spacing: 12,
|
||
margin: 100,
|
||
margin_bottom: 60
|
||
});
|
||
|
||
let label = new Gtk.Label({
|
||
label: '<span size="x-large">%s</span>'.format(
|
||
_("Something’s gone wrong")),
|
||
use_markup: true
|
||
});
|
||
label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
|
||
this.add(label);
|
||
|
||
label = new Gtk.Label({
|
||
label: _("We’re very sorry, but it was not possible to get the list of installed extensions. Make sure you are logged into GNOME and try again."),
|
||
justify: Gtk.Justification.CENTER,
|
||
wrap: true
|
||
});
|
||
this.add(label);
|
||
|
||
this.show_all();
|
||
}
|
||
});
|
||
|
||
var DescriptionLabel = GObject.registerClass(
|
||
class DescriptionLabel extends Gtk.Label {
|
||
vfunc_get_preferred_height_for_width(width) {
|
||
// Hack: Request the maximum height allowed by the line limit
|
||
if (this.lines > 0)
|
||
return super.vfunc_get_preferred_height_for_width(0);
|
||
return super.vfunc_get_preferred_height_for_width(width);
|
||
}
|
||
});
|
||
|
||
var ExtensionRow = GObject.registerClass(
|
||
class ExtensionRow extends Gtk.ListBoxRow {
|
||
_init(extension) {
|
||
super._init();
|
||
|
||
this._app = Gio.Application.get_default();
|
||
this._extension = extension;
|
||
this._prefsModule = null;
|
||
|
||
this._extensionStateChangedId = this._app.shellProxy.connectSignal(
|
||
'ExtensionStateChanged', (p, sender, [uuid, newState]) => {
|
||
if (this.uuid !== uuid)
|
||
return;
|
||
|
||
this._extension = ExtensionUtils.deserializeExtension(newState);
|
||
let state = (this._extension.state == ExtensionState.ENABLED);
|
||
this._switch.state = state;
|
||
this._switch.sensitive = this._canToggle();
|
||
});
|
||
|
||
this.connect('destroy', this._onDestroy.bind(this));
|
||
|
||
this._buildUI();
|
||
}
|
||
|
||
get uuid() {
|
||
return this._extension.uuid;
|
||
}
|
||
|
||
get name() {
|
||
return this._extension.metadata.name;
|
||
}
|
||
|
||
get hasPrefs() {
|
||
return this._extension.hasPrefs;
|
||
}
|
||
|
||
get url() {
|
||
return this._extension.metadata.url;
|
||
}
|
||
|
||
_onDestroy() {
|
||
if (!this._app.shellProxy)
|
||
return;
|
||
|
||
if (this._extensionStateChangedId)
|
||
this._app.shellProxy.disconnectSignal(this._extensionStateChangedId);
|
||
this._extensionStateChangedId = 0;
|
||
}
|
||
|
||
_buildUI() {
|
||
let hbox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL,
|
||
hexpand: true, margin_end: 24, spacing: 24,
|
||
margin: 12 });
|
||
this.add(hbox);
|
||
|
||
let vbox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL,
|
||
spacing: 6, hexpand: true });
|
||
hbox.add(vbox);
|
||
|
||
let name = GLib.markup_escape_text(this.name, -1);
|
||
let label = new Gtk.Label({ label: '<b>' + name + '</b>',
|
||
use_markup: true,
|
||
halign: Gtk.Align.START });
|
||
vbox.add(label);
|
||
|
||
let desc = this._extension.metadata.description.split('\n')[0];
|
||
label = new DescriptionLabel({ label: desc, wrap: true, lines: 2,
|
||
ellipsize: Pango.EllipsizeMode.END,
|
||
xalign: 0, yalign: 0 });
|
||
vbox.add(label);
|
||
|
||
let button = new Gtk.Button({ valign: Gtk.Align.CENTER,
|
||
visible: this.hasPrefs,
|
||
no_show_all: true });
|
||
button.set_image(new Gtk.Image({ icon_name: 'emblem-system-symbolic',
|
||
icon_size: Gtk.IconSize.BUTTON,
|
||
visible: true }));
|
||
button.get_style_context().add_class('circular');
|
||
hbox.add(button);
|
||
|
||
this.prefsButton = button;
|
||
|
||
this._switch = new Gtk.Switch({
|
||
valign: Gtk.Align.CENTER,
|
||
sensitive: this._canToggle(),
|
||
state: this._extension.state === ExtensionState.ENABLED
|
||
});
|
||
this._switch.connect('notify::active', () => {
|
||
if (this._switch.active)
|
||
this._app.shellProxy.EnableExtensionRemote(this.uuid);
|
||
else
|
||
this._app.shellProxy.DisableExtensionRemote(this.uuid);
|
||
});
|
||
this._switch.connect('state-set', () => true);
|
||
hbox.add(this._switch);
|
||
}
|
||
|
||
_canToggle() {
|
||
return this._extension.canChange;
|
||
}
|
||
|
||
get prefsModule() {
|
||
if (!this._prefsModule) {
|
||
ExtensionUtils.installImporter(this._extension);
|
||
|
||
// give extension prefs access to their own extension object
|
||
ExtensionUtils.getCurrentExtension = () => this._extension;
|
||
|
||
this._prefsModule = this._extension.imports.prefs;
|
||
this._prefsModule.init(this._extension.metadata);
|
||
}
|
||
|
||
return this._prefsModule;
|
||
}
|
||
});
|
||
|
||
function initEnvironment() {
|
||
// Monkey-patch in a "global" object that fakes some Shell utilities
|
||
// that ExtensionUtils depends on.
|
||
window.global = {
|
||
log(...args) {
|
||
print(args.join(', '));
|
||
},
|
||
|
||
logError(s) {
|
||
log(`ERROR: ${s}`);
|
||
},
|
||
|
||
userdatadir: GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell'])
|
||
};
|
||
|
||
String.prototype.format = Format.format;
|
||
}
|
||
|
||
function main(argv) {
|
||
initEnvironment();
|
||
|
||
Gettext.bindtextdomain(Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
|
||
Gettext.textdomain(Config.GETTEXT_PACKAGE);
|
||
|
||
new Application().run(argv);
|
||
}
|