284 lines
9.5 KiB
JavaScript
Raw Normal View History

2024-07-08 22:46:35 +02:00
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
//
// SPDX-License-Identifier: GPL-2.0-or-later
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Gtk from 'gi://Gtk';
import Config from '../config.js';
let St = null; // St is not available for prefs.js importing this file.
try {
St = (await import('gi://St')).default;
} catch (e) { }
/**
* Get a themed icon, using fallbacks from GSConnect's GResource when necessary.
*
* @param {string} name - A themed icon name
* @return {Gio.Icon} A themed icon
*/
export function getIcon(name) {
if (getIcon._resource === undefined) {
// Setup the desktop icons
const settings = St.Settings.get();
getIcon._desktop = new Gtk.IconTheme();
getIcon._desktop.set_theme_name(settings.gtk_icon_theme);
settings.connect('notify::gtk-icon-theme', (settings_, key_) => {
getIcon._desktop.set_theme_name(settings_.gtk_icon_theme);
});
// Preload our fallbacks
const iconPath = 'resource://org/gnome/Shell/Extensions/GSConnect/icons';
const iconNames = [
'org.gnome.Shell.Extensions.GSConnect',
'org.gnome.Shell.Extensions.GSConnect-symbolic',
'computer-symbolic',
'laptop-symbolic',
'smartphone-symbolic',
'tablet-symbolic',
'tv-symbolic',
'phonelink-ring-symbolic',
'sms-symbolic',
];
getIcon._resource = {};
for (const iconName of iconNames) {
getIcon._resource[iconName] = new Gio.FileIcon({
file: Gio.File.new_for_uri(`${iconPath}/${iconName}.svg`),
});
}
}
// Check the desktop icon theme
if (getIcon._desktop.has_icon(name))
return new Gio.ThemedIcon({name: name});
// Check our GResource
if (getIcon._resource[name] !== undefined)
return getIcon._resource[name];
// Fallback to hoping it's in the theme somewhere
return new Gio.ThemedIcon({name: name});
}
/**
* Get the contents of a GResource file, replacing `@PACKAGE_DATADIR@` where
* necessary.
*
* @param {string} relativePath - A path relative to GSConnect's resource path
* @return {string} The file contents as a string
*/
function getResource(relativePath) {
try {
const bytes = Gio.resources_lookup_data(
GLib.build_filenamev([Config.APP_PATH, relativePath]),
Gio.ResourceLookupFlags.NONE
);
const source = new TextDecoder().decode(bytes.toArray());
return source.replace('@PACKAGE_DATADIR@', Config.PACKAGE_DATADIR);
} catch (e) {
logError(e, 'GSConnect');
return null;
}
}
/**
* Install file contents, to an absolute directory path.
*
* @param {string} dirname - An absolute directory path
* @param {string} basename - The file name
* @param {string} contents - The file contents
* @return {boolean} A success boolean
*/
function _installFile(dirname, basename, contents) {
try {
const filename = GLib.build_filenamev([dirname, basename]);
GLib.mkdir_with_parents(dirname, 0o755);
return GLib.file_set_contents(filename, contents);
} catch (e) {
logError(e, 'GSConnect');
return false;
}
}
/**
* Install file contents from a GResource, to an absolute directory path.
*
* @param {string} dirname - An absolute directory path
* @param {string} basename - The file name
* @param {string} relativePath - A path relative to GSConnect's resource path
* @return {boolean} A success boolean
*/
function _installResource(dirname, basename, relativePath) {
try {
const contents = getResource(relativePath);
return _installFile(dirname, basename, contents);
} catch (e) {
logError(e, 'GSConnect');
return false;
}
}
/**
* Use Gio.File to ensure a file's executable bits are set.
*
* @param {string} filepath - An absolute path to a file
* @returns {boolean} - True if the file already was, or is now, executable
*/
function _setExecutable(filepath) {
try {
const file = Gio.File.new_for_path(filepath);
const finfo = file.query_info(
`${Gio.FILE_ATTRIBUTE_STANDARD_TYPE},${Gio.FILE_ATTRIBUTE_UNIX_MODE}`,
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
null);
if (!finfo.has_attribute(Gio.FILE_ATTRIBUTE_UNIX_MODE))
return false;
const mode = finfo.get_attribute_uint32(
Gio.FILE_ATTRIBUTE_UNIX_MODE);
const new_mode = (mode | 0o111);
if (mode === new_mode)
return true;
return file.set_attribute_uint32(
Gio.FILE_ATTRIBUTE_UNIX_MODE,
new_mode,
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
null);
} catch (e) {
logError(e, 'GSConnect');
return false;
}
}
/**
* Ensure critical files in the extension directory have the
* correct permissions.
*/
export function ensurePermissions() {
if (Config.IS_USER) {
const executableFiles = [
'gsconnect-preferences',
'service/daemon.js',
'service/nativeMessagingHost.js',
];
for (const file of executableFiles)
_setExecutable(GLib.build_filenamev([Config.PACKAGE_DATADIR, file]));
}
}
/**
* Install the files necessary for the GSConnect service to run.
*/
export function installService() {
const settings = new Gio.Settings({
settings_schema: Config.GSCHEMA.lookup(
'org.gnome.Shell.Extensions.GSConnect',
null
),
path: '/org/gnome/shell/extensions/gsconnect/',
});
const confDir = GLib.get_user_config_dir();
const dataDir = GLib.get_user_data_dir();
const homeDir = GLib.get_home_dir();
// DBus Service
const dbusDir = GLib.build_filenamev([dataDir, 'dbus-1', 'services']);
const dbusFile = `${Config.APP_ID}.service`;
// Desktop Entry
const appDir = GLib.build_filenamev([dataDir, 'applications']);
const appFile = `${Config.APP_ID}.desktop`;
const appPrefsFile = `${Config.APP_ID}.Preferences.desktop`;
// Application Icon
const iconDir = GLib.build_filenamev([dataDir, 'icons', 'hicolor', 'scalable', 'apps']);
const iconFull = `${Config.APP_ID}.svg`;
const iconSym = `${Config.APP_ID}-symbolic.svg`;
// File Manager Extensions
const fileManagers = [
[`${dataDir}/nautilus-python/extensions`, 'nautilus-gsconnect.py'],
[`${dataDir}/nemo-python/extensions`, 'nemo-gsconnect.py'],
];
// WebExtension Manifests
const manifestFile = 'org.gnome.shell.extensions.gsconnect.json';
const google = getResource(`webextension/${manifestFile}.google.in`);
const mozilla = getResource(`webextension/${manifestFile}.mozilla.in`);
const manifests = [
[`${confDir}/chromium/NativeMessagingHosts/`, google],
[`${confDir}/google-chrome/NativeMessagingHosts/`, google],
[`${confDir}/google-chrome-beta/NativeMessagingHosts/`, google],
[`${confDir}/google-chrome-unstable/NativeMessagingHosts/`, google],
[`${confDir}/BraveSoftware/Brave-Browser/NativeMessagingHosts/`, google],
[`${confDir}/BraveSoftware/Brave-Browser-Beta/NativeMessagingHosts/`, google],
[`${confDir}/BraveSoftware/Brave-Browser-Nightly/NativeMessagingHosts/`, google],
[`${homeDir}/.mozilla/native-messaging-hosts/`, mozilla],
[`${homeDir}/.config/microsoft-edge-dev/NativeMessagingHosts`, google],
[`${homeDir}/.config/microsoft-edge-beta/NativeMessagingHosts`, google],
];
// If running as a user extension, ensure the DBus service, desktop entry,
// file manager scripts, and WebExtension manifests are installed.
if (Config.IS_USER) {
// DBus Service
if (!_installResource(dbusDir, dbusFile, `${dbusFile}.in`))
throw Error('GSConnect: Failed to install DBus Service');
// Desktop Entries
_installResource(appDir, appFile, appFile);
_installResource(appDir, appPrefsFile, appPrefsFile);
// Application Icon
_installResource(iconDir, iconFull, `icons/${iconFull}`);
_installResource(iconDir, iconSym, `icons/${iconSym}`);
// File Manager Extensions
const target = `${Config.PACKAGE_DATADIR}/nautilus-gsconnect.py`;
for (const [dir, name] of fileManagers) {
const script = Gio.File.new_for_path(GLib.build_filenamev([dir, name]));
if (!script.query_exists(null)) {
GLib.mkdir_with_parents(dir, 0o755);
script.make_symbolic_link(target, null);
}
}
// WebExtension Manifests
if (settings.get_boolean('create-native-messaging-hosts')) {
for (const [dirname, contents] of manifests)
_installFile(dirname, manifestFile, contents);
}
// Otherwise, if running as a system extension, ensure anything previously
// installed when running as a user extension is removed.
} else {
GLib.unlink(GLib.build_filenamev([dbusDir, dbusFile]));
GLib.unlink(GLib.build_filenamev([appDir, appFile]));
GLib.unlink(GLib.build_filenamev([appDir, appPrefsFile]));
GLib.unlink(GLib.build_filenamev([iconDir, iconFull]));
GLib.unlink(GLib.build_filenamev([iconDir, iconSym]));
for (const [dir, name] of fileManagers)
GLib.unlink(GLib.build_filenamev([dir, name]));
for (const manifest of manifests)
GLib.unlink(GLib.build_filenamev([manifest[0], manifestFile]));
}
}