1026 lines
32 KiB
JavaScript
1026 lines
32 KiB
JavaScript
/* DING: Desktop Icons New Generation for GNOME Shell
|
|
*
|
|
* Copyright (C) 2022 Sundeep Mediratta - eslint fix errors and format GJS/Gnome guidelines
|
|
* Copyright (C) 2019 Sergio Costas (rastersoft@gmail.com)
|
|
* Based on code original (C) Carlos Soriano
|
|
*
|
|
* 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, version 3 of the License.
|
|
*
|
|
* 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, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
import {Gio, GLib, Gdk, GdkX11, DesktopAppInfo} from '../../dependencies/gi.js';
|
|
import {_} from '../../dependencies/gettext.js';
|
|
|
|
export {DesktopIconsUtil};
|
|
|
|
const DesktopIconsUtil = class {
|
|
constructor(Data, Utils) {
|
|
this.mainApp = Data.mainApp;
|
|
this.Enums = Data.Enums;
|
|
this.FileUtils = Utils.FileUtils;
|
|
this.Prefs = Utils.Preferences;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returs the Gtk Application ID
|
|
*/
|
|
getMainApp() {
|
|
return this.mainApp;
|
|
}
|
|
|
|
|
|
usingX11() {
|
|
return Gdk.Display.get_default() instanceof GdkX11.X11Display;
|
|
}
|
|
|
|
ensureDir(path) {
|
|
const file = Gio.File.new_for_path(path);
|
|
try {
|
|
file.make_directory_with_parents(null);
|
|
} catch (e) {
|
|
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
|
|
throw e;
|
|
}
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Returns the Nautilus scripts directory as a Gio.File
|
|
*/
|
|
getScriptsDir() {
|
|
return Gio.File.new_build_filenamev([
|
|
GLib.get_home_dir(),
|
|
this.Enums.NAUTILUS_SCRIPTS_DIR,
|
|
]);
|
|
}
|
|
|
|
getUserTerminalConfFile() {
|
|
const xdgUserConfigFolder = GLib.get_user_config_dir();
|
|
|
|
return Gio.File.new_build_filenamev([
|
|
xdgUserConfigFolder,
|
|
this.Enums.XDG_TERMINAL_LIST_FILE,
|
|
]);
|
|
}
|
|
|
|
getSystemTerminalConfFile() {
|
|
const xdgEtcConfigFolder = GLib.get_system_config_dirs();
|
|
const systemTerminalConfFiles = [];
|
|
|
|
xdgEtcConfigFolder.forEach(f => {
|
|
const gioFile =
|
|
Gio.File.new_build_filenamev([
|
|
f,
|
|
this.Enums.XDG_TERMINAL_LIST_FILE,
|
|
]);
|
|
|
|
systemTerminalConfFiles.push(gioFile);
|
|
});
|
|
|
|
return systemTerminalConfFiles;
|
|
}
|
|
|
|
getUserDataTerminalDir() {
|
|
const userDataDir = GLib.get_user_data_dir();
|
|
|
|
return Gio.File.new_build_filenamev([
|
|
userDataDir,
|
|
this.Enums.XDG_TERMINAL_DIR,
|
|
this.Enums.XDG_TERMINAL_LIST_FILE,
|
|
]);
|
|
}
|
|
|
|
getSystemDataTerminalDirs() {
|
|
const systemDataDirs = this.Enums.SYSTEM_DATA_DIRS;
|
|
const systemDataTerminalFiles = [];
|
|
|
|
systemDataDirs.forEach(f => {
|
|
const gioFile = Gio.File.new_build_filenamev([
|
|
f,
|
|
this.Enums.XDG_TERMINAL_DIR,
|
|
this.Enums.XDG_TERMINAL_LIST_FILE,
|
|
]);
|
|
systemDataTerminalFiles.push(gioFile);
|
|
});
|
|
|
|
return systemDataTerminalFiles;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Returns the users Templates directory as a Gio.File
|
|
*/
|
|
getTemplatesDir() {
|
|
let templatesDir =
|
|
GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_TEMPLATES);
|
|
|
|
if ((templatesDir === GLib.get_home_dir()) || (templatesDir === null))
|
|
return null;
|
|
|
|
return Gio.File.new_for_commandline_arg(templatesDir);
|
|
}
|
|
|
|
|
|
/**
|
|
* XDG user data dir for this app:
|
|
* $XDG_DATA_HOME/<app-id> (usually ~/.local/share/<app-id>)
|
|
*
|
|
* @returns {Gio.File}
|
|
*/
|
|
getAppUserDataDir() {
|
|
const userDataDir = GLib.get_user_data_dir();
|
|
const appId = this.mainApp?.get_application_id
|
|
? this.mainApp.get_application_id()
|
|
: null;
|
|
|
|
if (!appId)
|
|
throw new Error('Application ID is not available');
|
|
|
|
return Gio.File.new_build_filenamev([userDataDir, appId]);
|
|
}
|
|
|
|
/**
|
|
* Widgets state file under the app data dir:
|
|
* $XDG_DATA_HOME/<app-id>/widgets.json
|
|
*
|
|
* @returns {Gio.File}
|
|
*/
|
|
getWidgetsStateFile() {
|
|
const appDir = this.getAppUserDataDir();
|
|
return appDir.get_child('widgets.json');
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {float} value number
|
|
* @param {integer} min number
|
|
* @param {integer} max number
|
|
*/
|
|
clamp(value, min, max) {
|
|
return Math.max(Math.min(value, max), min);
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
*/
|
|
getFilteredEnviron() {
|
|
let environ = [];
|
|
for (let env of GLib.get_environ()) {
|
|
/* It's a must to remove the WAYLAND_SOCKET environment variable
|
|
because, under Wayland, DING uses an specific socket to allow the
|
|
extension to detect its windows. But the scripts must run under
|
|
the normal socket */
|
|
if (env.startsWith('WAYLAND_SOCKET='))
|
|
continue;
|
|
|
|
environ.push(env);
|
|
}
|
|
return environ;
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {string} commandLine command to execute
|
|
* @param {Array} environ child's environment, or <code>null</code>
|
|
* to inherit parent's
|
|
*/
|
|
spawnCommandLine(commandLine, environ = null) {
|
|
try {
|
|
const [, argv] = GLib.shell_parse_argv(commandLine);
|
|
this.trySpawn(null, argv, environ);
|
|
} catch (e) {
|
|
console.error(e, `${commandLine} failed with ${e}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} workdir working directory path
|
|
* @param {Array(String)} argv child's argument vector
|
|
* @param {Array} environ child's environment, or <code>null</code> to
|
|
* inherit parent's
|
|
* @param {bool} async or async execution
|
|
*/
|
|
trySpawn(workdir, argv, environ = null, async = true) {
|
|
/* The following code has been extracted from GNOME Shell's
|
|
* source code in Misc.Util.trySpawn function and modified to
|
|
* set the working directory.
|
|
*
|
|
* https://gitlab.gnome.org/GNOME/gnome-shell/blob/gnome-3-30/js/misc/util.js
|
|
*/
|
|
const exec = async ? GLib.spawn_async : GLib.spawn_sync;
|
|
const flags =
|
|
async
|
|
? GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD
|
|
: GLib.SpawnFlags.SEARCH_PATH;
|
|
|
|
var pid;
|
|
|
|
try {
|
|
[, pid] = exec(workdir, argv, environ, flags,
|
|
() => {});
|
|
} 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;
|
|
}
|
|
}
|
|
|
|
if (!async)
|
|
return;
|
|
|
|
// 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, () => {});
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {float} x first x coordinate
|
|
* @param {float} y first y coordinate
|
|
* @param {float} x2 second x coordinate
|
|
* @param {float} y2 second y coordinate
|
|
* @returns {float} Distance between points
|
|
*/
|
|
distanceBetweenPoints(x, y, x2, y2) {
|
|
return Math.pow(x - x2, 2) + Math.pow(y - y2, 2);
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
*/
|
|
getExtraFolders() {
|
|
const extraFolders = [];
|
|
|
|
if (this.Prefs.desktopSettings.get_boolean('show-home')) {
|
|
extraFolders.push(
|
|
[
|
|
Gio.File.new_for_commandline_arg(GLib.get_home_dir()),
|
|
this.Enums.FileType.USER_DIRECTORY_HOME,
|
|
]
|
|
);
|
|
}
|
|
|
|
if (this.Prefs.desktopSettings.get_boolean('show-trash')) {
|
|
extraFolders.push(
|
|
[
|
|
Gio.File.new_for_uri('trash:///'),
|
|
this.Enums.FileType.USER_DIRECTORY_TRASH,
|
|
]
|
|
);
|
|
}
|
|
|
|
return extraFolders;
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {Gio.VolumeMonitor} volumeMonitor A Gio.VolumeMonitor
|
|
*/
|
|
getMounts(volumeMonitor) {
|
|
const showVolumes =
|
|
this.Prefs.desktopSettings.get_boolean('show-volumes');
|
|
|
|
const showNetwork =
|
|
this.Prefs.desktopSettings.get_boolean('show-network-volumes');
|
|
|
|
var mountedFileSystems;
|
|
|
|
try {
|
|
mountedFileSystems = volumeMonitor.get_mounts();
|
|
} catch (e) {
|
|
console.log(`Failed to get the list of mounts with ${e}`);
|
|
return [];
|
|
}
|
|
|
|
let result = [];
|
|
let uris = [];
|
|
|
|
for (let gioMount of mountedFileSystems) {
|
|
try {
|
|
let isDrive =
|
|
(gioMount.get_drive() !== null) ||
|
|
(gioMount.get_volume() !== null);
|
|
|
|
let uri = gioMount.get_default_location().get_uri();
|
|
|
|
if (((isDrive && showVolumes) ||
|
|
(!isDrive && showNetwork)) &&
|
|
!uris.includes(uri)
|
|
) {
|
|
result.push([
|
|
gioMount.get_default_location(),
|
|
this.Enums.FileType.EXTERNAL_DRIVE,
|
|
gioMount,
|
|
]);
|
|
|
|
uris.push(uri);
|
|
}
|
|
} catch (e) {
|
|
console.log(`Failed with ${e} while getting volume`);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {string} filename Name of file
|
|
* @param {object} opts Oject with boolean option keys
|
|
*/
|
|
getFileExtensionOffset(filename, opts = {'isDirectory': false}) {
|
|
let offset = filename.length;
|
|
let extension = '';
|
|
|
|
if (!opts.isDirectory) {
|
|
const doubleExtensions =
|
|
['.gz', '.bz2', '.sit', '.Z', '.bz', '.xz'];
|
|
|
|
for (const item of doubleExtensions) {
|
|
if (filename.endsWith(item)) {
|
|
offset -= item.length;
|
|
extension = filename.substring(offset);
|
|
filename = filename.substring(0, offset);
|
|
break;
|
|
}
|
|
}
|
|
|
|
let lastDot = filename.lastIndexOf('.');
|
|
|
|
if (lastDot > 0) {
|
|
offset = lastDot;
|
|
extension = filename.substring(offset) + extension;
|
|
filename = filename.substring(0, offset);
|
|
}
|
|
}
|
|
|
|
return {offset, 'basename': filename, extension};
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {Gio.File} file a file Gio
|
|
* @param {stirng} contents file contents
|
|
* @param {Gio.Cancellable} cancellable gio cancellable
|
|
*/
|
|
replaceFileContentsAsync(file, contents, cancellable) {
|
|
/* Promisify doesn't work with this */
|
|
const textCoder = new TextEncoder();
|
|
const byteArray = new GLib.Bytes(textCoder.encode(contents));
|
|
|
|
return new Promise((resolve, reject) => {
|
|
file.replace_contents_bytes_async(
|
|
byteArray,
|
|
null,
|
|
true,
|
|
Gio.FileCreateFlags.REPLACE_DESTINATION,
|
|
cancellable,
|
|
(sourceObject, res) => {
|
|
try {
|
|
resolve(file.replace_contents_finish(res));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Gio.File} file a file Gio
|
|
* @param {Gio.Cancellable} cancellable gio cancellable
|
|
*/
|
|
readFileContentsAsync(file, cancellable = null) {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
file.read_async(
|
|
GLib.PRIORITY_DEFAULT,
|
|
cancellable,
|
|
(actor, result) => {
|
|
try {
|
|
let inputstream = actor.read_finish(result);
|
|
|
|
let dataInputstream =
|
|
Gio.DataInputStream.new(inputstream);
|
|
|
|
let [string, number] =
|
|
dataInputstream.read_upto('', 0, cancellable);
|
|
|
|
if (number)
|
|
resolve(string);
|
|
else
|
|
reject(Error.new('Empty String'));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
reject(new Error('Error reading file'));
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Read up to `bytes` bytes from a Gio.File.
|
|
* If fromTail == false: read from start of file.
|
|
* If fromTail == true: read the last `bytes` bytes of the file.
|
|
*
|
|
* @param {Gio.File} file a file Gio
|
|
* @param {number} bytes number of bytes to read
|
|
* @param {Gio.Cancellable?} cancellable gio cancellable
|
|
* @param {boolean} fromTail read from tail if true else from start
|
|
* @returns {Promise<Gio.Bytes>}
|
|
*/
|
|
readFileBytesAsync(file, bytes, cancellable = null, fromTail = false) {
|
|
return new Promise((resolve, reject) => {
|
|
let offset = 0;
|
|
let newBytes = bytes;
|
|
|
|
if (fromTail) {
|
|
let info;
|
|
try {
|
|
info = file.query_info(
|
|
'standard::size',
|
|
Gio.FileQueryInfoFlags.NONE,
|
|
cancellable
|
|
);
|
|
} catch (e) {
|
|
reject(new Error(`Error getting file size: ${e}`));
|
|
return;
|
|
}
|
|
const fileSize = info.get_size();
|
|
offset = Math.max(0, fileSize - bytes);
|
|
newBytes = Math.min(bytes, fileSize);
|
|
}
|
|
|
|
try {
|
|
file.read_async(
|
|
GLib.PRIORITY_DEFAULT,
|
|
cancellable,
|
|
(actor, result) => {
|
|
let inputstream;
|
|
try {
|
|
inputstream = actor.read_finish(result);
|
|
} catch (e) {
|
|
reject(
|
|
new Error(`Error opening input stream: ${e}`)
|
|
);
|
|
return;
|
|
}
|
|
|
|
function failAndClose(message) {
|
|
try {
|
|
inputstream.close(cancellable);
|
|
} catch (e) {
|
|
// ignore close error
|
|
}
|
|
reject(new Error(message));
|
|
}
|
|
|
|
function doRead() {
|
|
inputstream.read_bytes_async(
|
|
newBytes,
|
|
GLib.PRIORITY_DEFAULT,
|
|
cancellable,
|
|
(sourceObject, res2) => {
|
|
let data;
|
|
try {
|
|
data =
|
|
sourceObject
|
|
.read_bytes_finish(res2);
|
|
} catch (e) {
|
|
failAndClose(
|
|
`Error reading bytes: ${e}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (data && data.get_size() > 0) {
|
|
try {
|
|
inputstream.close(cancellable);
|
|
} catch (e) {
|
|
/* ignore */
|
|
}
|
|
resolve(data);
|
|
return;
|
|
}
|
|
|
|
failAndClose('Empty Bytes');
|
|
}
|
|
);
|
|
}
|
|
|
|
if (offset === 0) {
|
|
doRead();
|
|
return;
|
|
}
|
|
|
|
inputstream.skip_async(
|
|
offset,
|
|
GLib.PRIORITY_DEFAULT,
|
|
cancellable,
|
|
(sourceObject, resSkip) => {
|
|
try {
|
|
const skipped =
|
|
sourceObject.skip_finish(resSkip);
|
|
|
|
if (skipped !== offset) {
|
|
failAndClose(
|
|
`Skip Error:
|
|
wanted offset ${offset} bytes,
|
|
got ${skipped} bytes`
|
|
);
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
failAndClose(`Error skipping bytes: ${e}`);
|
|
return;
|
|
}
|
|
doRead();
|
|
}
|
|
);
|
|
}
|
|
);
|
|
} catch (e) {
|
|
reject(new Error(`Error starting async read: ${e}`));
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if a pdf file is encrypted
|
|
* @param {Gio.File} file a file Gio of pdf file
|
|
* @param {Gio.Cancellable} cancellable gio cancellable
|
|
* @returns boolean
|
|
*/
|
|
async checkIfPdfEncrypted(file, cancellable = null) {
|
|
const fromTail = true;
|
|
const data = await this.readFileBytesAsync(
|
|
file,
|
|
1024,
|
|
cancellable,
|
|
fromTail
|
|
).catch(e => logError(e));
|
|
|
|
if (!data)
|
|
return false;
|
|
|
|
const decoder = new TextDecoder('latin1');
|
|
|
|
const trailerText = decoder.decode(data);
|
|
|
|
const hasEncrypt = trailerText.includes('/Encrypt');
|
|
if (!hasEncrypt)
|
|
return false;
|
|
|
|
const looksLikeClassicTrailer = trailerText.includes('trailer');
|
|
const looksLikeXrefStream =
|
|
trailerText.includes('/Type') && trailerText.includes('/XRef');
|
|
|
|
const encrypted = looksLikeClassicTrailer || looksLikeXrefStream;
|
|
|
|
return encrypted;
|
|
}
|
|
|
|
/**
|
|
* Check if a zip file is encrypted
|
|
* @param {Gio.File} file a file Gio of zip file
|
|
* @param {Gio.Cancellable} cancellable gio cancellable
|
|
* @returns boolean
|
|
*/
|
|
async checkIfZipEncrypted(file, cancellable = null) {
|
|
const data = await this.readFileBytesAsync(
|
|
file,
|
|
1024,
|
|
cancellable
|
|
).catch(e => logError(e));
|
|
|
|
if (!data)
|
|
return false;
|
|
|
|
// Zip Encryption single file archive
|
|
// Check if the 6th bit in the 7th byte (flag field) is set
|
|
const generalPurposeFlag = new Uint8Array([data.get_data()[6]]);
|
|
const zipEncrypted = (generalPurposeFlag & 0x01) === 0x01;
|
|
|
|
if (zipEncrypted)
|
|
return true;
|
|
|
|
// Multiple zip file archive- needs external checking
|
|
const command = `zipinfo -v '${file.get_path()}'`;
|
|
const decoder = new TextDecoder();
|
|
|
|
try {
|
|
const [, out,, status] = GLib.spawn_command_line_sync(command);
|
|
|
|
if (status !== 0)
|
|
return false;
|
|
|
|
const contents = decoder.decode(out);
|
|
return contents.trim().split('\n').some(l => {
|
|
return l.includes('file security status:') &&
|
|
!l.includes('not encrypted');
|
|
});
|
|
} catch (e) {
|
|
if (!e.matches(GLib.SpawnError, GLib.SpawnError.NOENT))
|
|
console.error(`Error determining encryption Zip file ${e}`);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a 7z file is encrypted
|
|
* @param {Gio.File} file a file Gio of 7z file
|
|
* @returns boolean
|
|
*/
|
|
checkIf7zEncrypted(file) {
|
|
const command = `7z l -pBadPassword -slt '${file.get_path()}'`;
|
|
const decoder = new TextDecoder();
|
|
|
|
try {
|
|
const [, out, error] = GLib.spawn_command_line_sync(command);
|
|
const contents = decoder.decode(error) + decoder.decode(out);
|
|
return contents.trim().split('\n').some(l => {
|
|
return l.includes('Encrypted = +') ||
|
|
l.includes('Wrong password?');
|
|
});
|
|
} catch (e) {
|
|
if (!e.matches(GLib.SpawnError, GLib.SpawnError.NOENT))
|
|
console.error(`Error determining encryption 7z file ${e}`);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} fileList text with list of Terminals, new line terminated
|
|
* @returns {Array} an array of Gio.DesktopAppInfo for each desktop
|
|
* entry in file
|
|
*/
|
|
parseTerminalList(fileList) {
|
|
const regexpattern = /^[/\\*#-]/;
|
|
const terminalGioDesktopAppInfoArray = [];
|
|
|
|
if (fileList.endsWith('\n'))
|
|
fileList = fileList.slice(0, -1);
|
|
|
|
const fileListArray =
|
|
fileList.split('\n').filter(f => !f.match(regexpattern));
|
|
|
|
if (fileListArray.length) {
|
|
fileListArray.forEach(f => {
|
|
const appinfo = DesktopAppInfo.new(f.replace('+', ''));
|
|
if (appinfo)
|
|
terminalGioDesktopAppInfoArray.push(appinfo);
|
|
});
|
|
}
|
|
|
|
return terminalGioDesktopAppInfoArray;
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {string} content text of the user-dirs.dirs file
|
|
* @returns {string} path of Desktop Directory
|
|
*/
|
|
parseUserDirsFile(content) {
|
|
if (!content)
|
|
return null;
|
|
|
|
const lineArray = content.trim().split('\n');
|
|
|
|
const desktopline =
|
|
lineArray.filter(l => l.startsWith('XDG_DESKTOP_DIR='))[0];
|
|
|
|
let xdgDesktopPath = desktopline.split('=')[1].trim();
|
|
xdgDesktopPath = xdgDesktopPath.replace(/^"|"$/g, '');
|
|
xdgDesktopPath = xdgDesktopPath.replace('$HOME', GLib.get_home_dir());
|
|
|
|
return xdgDesktopPath;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} text text to write in the file
|
|
* @param {string} destinationDir path
|
|
* @param {string} filename name of file
|
|
* @param {Array(integer)} dropCoordinates coordiantes for the dropped file
|
|
* @param {Gio.Cancellable} cancellable a Gio.Cancellable
|
|
*/
|
|
async writeTextFileToPath(text, destinationDir, filename,
|
|
dropCoordinates, cancellable = null) {
|
|
const file = destinationDir.get_child(filename);
|
|
|
|
try {
|
|
await this.FileUtils
|
|
.recursivelyMakeDir(destinationDir, cancellable);
|
|
|
|
const info = new Gio.FileInfo();
|
|
info.set_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE, 0o700);
|
|
|
|
await destinationDir.set_attributes_async(
|
|
info,
|
|
Gio.FileQueryInfoFlags.NONE,
|
|
GLib.PRIORITY_NORMAL,
|
|
cancellable
|
|
);
|
|
} catch (e) {
|
|
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
|
|
throw e;
|
|
}
|
|
|
|
await this.replaceFileContentsAsync(file, text, cancellable);
|
|
|
|
if (dropCoordinates !== null) {
|
|
const info = new Gio.FileInfo();
|
|
info.set_attribute_string('metadata::nautilus-drop-position',
|
|
`${dropCoordinates.join(',')}`);
|
|
|
|
await file.set_attributes_async(
|
|
info,
|
|
Gio.FileQueryInfoFlags.NONE,
|
|
GLib.PRIORITY_LOW,
|
|
cancellable
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} fileUri The system file URI of the .desktop
|
|
* file of the installed application
|
|
*
|
|
* @param {Array} dropCoordinates the drop cooordinates of the .desktop file
|
|
*
|
|
* Makes an executable .desktop file on the desktop with metadata set trusted.
|
|
*/
|
|
copyDesktopFileToDesktop(fileUri, dropCoordinates) {
|
|
return new Promise((resolve, reject) => {
|
|
let gioFile = Gio.File.new_for_uri(fileUri);
|
|
|
|
let destinationGioFile = Gio.File.new_build_filenamev([
|
|
GLib.get_user_special_dir(
|
|
GLib.UserDirectory.DIRECTORY_DESKTOP
|
|
),
|
|
gioFile.get_basename(),
|
|
]);
|
|
|
|
let gioFileCopyFlags =
|
|
Gio.FileCopyFlags.OVERWRITE |
|
|
Gio.FileCopyFlags.TARGET_DEFAULT_PERMS;
|
|
|
|
gioFile.copy_async(
|
|
destinationGioFile,
|
|
gioFileCopyFlags,
|
|
GLib.PRIORITY_LOW,
|
|
null,
|
|
null,
|
|
(source, result) => {
|
|
try {
|
|
let res = source.copy_finish(result);
|
|
|
|
if (res) {
|
|
let info = new Gio.FileInfo();
|
|
|
|
if (dropCoordinates !== null) {
|
|
info.set_attribute_string(
|
|
'metadata::nautilus-drop-position',
|
|
`${dropCoordinates[0]},${dropCoordinates[1]}`
|
|
);
|
|
}
|
|
|
|
let newUnixMode =
|
|
this.Enums.UnixPermissions.S_IRUSR |
|
|
this.Enums.UnixPermissions.S_IWUSR |
|
|
this.Enums.UnixPermissions.S_IXUSR |
|
|
this.Enums.UnixPermissions.S_IRGRP |
|
|
this.Enums.UnixPermissions.S_IWGRP |
|
|
this.Enums.UnixPermissions.S_IROTH;
|
|
|
|
info.set_attribute_uint32(
|
|
Gio.FILE_ATTRIBUTE_UNIX_MODE, newUnixMode
|
|
);
|
|
|
|
info.set_attribute_string(
|
|
'metadata::trusted', 'true'
|
|
);
|
|
|
|
destinationGioFile.set_attributes_async(
|
|
info,
|
|
Gio.FileQueryInfoFlags.NONE,
|
|
GLib.PRIORITY_LOW,
|
|
null,
|
|
(sour, resul) => {
|
|
try {
|
|
resolve(
|
|
sour.set_attributes_finish(resul)
|
|
);
|
|
} catch (error) {
|
|
console.log(
|
|
'Failed to make executable ' +
|
|
`.desktop File: ${error.message}`
|
|
);
|
|
|
|
reject(error);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
|
|
reject(e);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Gtk.Window} window The window
|
|
* @param {boolean} modal If the window should be modal
|
|
*/
|
|
windowHidePagerTaskbarModal(window, modal) {
|
|
window.set_application(this.mainApp);
|
|
let title = window.get_title();
|
|
if (title === null)
|
|
title = '';
|
|
|
|
if (modal)
|
|
title += ' ';
|
|
else
|
|
title += ' ';
|
|
|
|
window.set_title(title);
|
|
|
|
if (modal) {
|
|
window.set_modal(true);
|
|
window.grab_focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {integer} ms milliseconds
|
|
*/
|
|
waitDelayMs(ms) {
|
|
return new Promise(resolve => {
|
|
GLib.timeout_add(GLib.PRIORITY_DEFAULT, ms, () => {
|
|
resolve();
|
|
return false;
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Coordiantes are the same
|
|
*
|
|
* @param {Array(integer)} coordA coordinates
|
|
* @param {Array(integer)} coordB coordinates
|
|
* @returns {boolean} true or false
|
|
*/
|
|
coordinatesEqual(coordA, coordB) {
|
|
if (coordA === coordB)
|
|
return true;
|
|
|
|
if (coordA && coordB)
|
|
return (coordA[0] === coordB[0]) && (coordA[1] === coordB[1]);
|
|
|
|
return false;
|
|
}
|
|
|
|
checkAppOpensFileType(
|
|
gioDesktopAppInfo,
|
|
fileUri = null,
|
|
attributeContentType = null
|
|
) {
|
|
let Appname = gioDesktopAppInfo.get_name();
|
|
let gioFileInfo;
|
|
let AppsSupportingOpen = [];
|
|
|
|
if (fileUri) {
|
|
gioFileInfo =
|
|
Gio.File.new_for_uri(fileUri)
|
|
.query_info(
|
|
this.Enums.DEFAULT_ATTRIBUTES,
|
|
Gio.FileQueryInfoFlags.NONE,
|
|
null
|
|
);
|
|
|
|
AppsSupportingOpen =
|
|
Gio.AppInfo.get_all_for_type(gioFileInfo.get_content_type());
|
|
} else if (attributeContentType) {
|
|
AppsSupportingOpen =
|
|
Gio.AppInfo.get_all_for_type(attributeContentType);
|
|
} else {
|
|
return {canopenFile: false, Appname: 'None'};
|
|
}
|
|
|
|
AppsSupportingOpen = AppsSupportingOpen.map(f => f.get_name());
|
|
|
|
let canopenFile;
|
|
|
|
if (AppsSupportingOpen.includes(Appname))
|
|
canopenFile = true;
|
|
else
|
|
canopenFile = false;
|
|
|
|
return {canopenFile, Appname};
|
|
}
|
|
|
|
/**
|
|
* Read JSON from a file. Returns parsed object or null on error.
|
|
*
|
|
* @param {Gio.File} file
|
|
* @param {Gio.Cancellable?} cancellable
|
|
* @returns {Promise<object|null>}
|
|
*/
|
|
async readJsonFile(file, cancellable = null) {
|
|
try {
|
|
const text = await this.readFileContentsAsync(file, cancellable);
|
|
if (!text)
|
|
return null;
|
|
|
|
return JSON.parse(text);
|
|
} catch (e) {
|
|
if (!Gio.IOErrorEnum.matches(e, Gio.IOErrorEnum.NOT_FOUND))
|
|
console.error(`readJsonFile(${file.get_path?.() ?? '??'}):`, e);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write JSON to a file (pretty-printed).
|
|
*
|
|
* Mirrors writeTextFileToPath() by ensuring the parent dir via FileUtils.
|
|
*
|
|
* @param {Gio.File} file
|
|
* @param {object} data
|
|
* @param {Gio.Cancellable?} cancellable
|
|
*/
|
|
async writeJsonFile(file, data, cancellable = null) {
|
|
const parent = file.get_parent();
|
|
|
|
if (parent) {
|
|
try {
|
|
await this.FileUtils.recursivelyMakeDir(parent, cancellable);
|
|
} catch (e) {
|
|
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
let text;
|
|
try {
|
|
text = JSON.stringify(data, null, 2);
|
|
} catch (e) {
|
|
console.error('writeJsonFile: JSON.stringify failed:', e);
|
|
throw e;
|
|
}
|
|
|
|
await this.replaceFileContentsAsync(file, text, cancellable);
|
|
}
|
|
};
|