325 lines
8.7 KiB
JavaScript
325 lines
8.7 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const Gdk = imports.gi.Gdk;
|
||
|
const Gio = imports.gi.Gio;
|
||
|
const GLib = imports.gi.GLib;
|
||
|
const Gtk = imports.gi.Gtk;
|
||
|
|
||
|
const ByteArray = imports.byteArray;
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Check if we're in a Wayland session (mostly for input synthesis)
|
||
|
* https://wiki.gnome.org/Accessibility/Wayland#Bugs.2FIssues_We_Must_Address
|
||
|
*/
|
||
|
window._WAYLAND = GLib.getenv('XDG_SESSION_TYPE') === 'wayland';
|
||
|
window.HAVE_REMOTEINPUT = GLib.getenv('GDMSESSION') !== 'ubuntu-wayland';
|
||
|
|
||
|
|
||
|
/**
|
||
|
* A custom debug function that logs at LEVEL_MESSAGE to avoid the need for env
|
||
|
* variables to be set.
|
||
|
*
|
||
|
* @param {Error|string} message - A string or Error to log
|
||
|
* @param {string} [prefix] - An optional prefix for the warning
|
||
|
*/
|
||
|
const _debugFunc = function(message, prefix = null) {
|
||
|
let caller;
|
||
|
|
||
|
if (message.stack) {
|
||
|
caller = message.stack.split('\n')[0];
|
||
|
message = `${message.message}\n${message.stack}`;
|
||
|
} else {
|
||
|
message = JSON.stringify(message, null, 2);
|
||
|
caller = (new Error()).stack.split('\n')[1];
|
||
|
}
|
||
|
|
||
|
// Prepend prefix
|
||
|
message = (prefix) ? `${prefix}: ${message}` : message;
|
||
|
|
||
|
// Cleanup the stack
|
||
|
let [, func, file, line] = caller.match(/([^@]*)@([^:]*):([^:]*)/);
|
||
|
let script = file.replace(gsconnect.extdatadir, '');
|
||
|
|
||
|
GLib.log_structured('GSConnect', GLib.LogLevelFlags.LEVEL_MESSAGE, {
|
||
|
'MESSAGE': `[${script}:${func}:${line}]: ${message}`,
|
||
|
'SYSLOG_IDENTIFIER': 'org.gnome.Shell.Extensions.GSConnect',
|
||
|
'CODE_FILE': file,
|
||
|
'CODE_FUNC': func,
|
||
|
'CODE_LINE': line
|
||
|
});
|
||
|
};
|
||
|
|
||
|
// Swap the function out for a no-op anonymous function for speed
|
||
|
window.debug = gsconnect.settings.get_boolean('debug') ? _debugFunc : () => {};
|
||
|
|
||
|
gsconnect.settings.connect('changed::debug', (settings) => {
|
||
|
window.debug = settings.get_boolean('debug') ? _debugFunc : () => {};
|
||
|
});
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Convenience function for loading JSON from a file
|
||
|
*
|
||
|
* @param {Gio.File|string} file - A Gio.File or path to a JSON file
|
||
|
* @param {boolean} sync - Default is %false, if %true load synchronously
|
||
|
* @return {object} - The parsed object
|
||
|
*/
|
||
|
JSON.load = function (file, sync = false) {
|
||
|
if (typeof file === 'string') {
|
||
|
file = Gio.File.new_for_path(file);
|
||
|
}
|
||
|
|
||
|
if (sync) {
|
||
|
let contents = file.load_contents(null)[1];
|
||
|
|
||
|
return JSON.parse(ByteArray.toString(contents));
|
||
|
} else {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
file.load_contents_async(null, (file, res) => {
|
||
|
try {
|
||
|
let contents = file.load_contents_finish(res)[1];
|
||
|
|
||
|
resolve(JSON.parse(ByteArray.toString(contents)));
|
||
|
} catch (e) {
|
||
|
reject(e);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Convenience function for dumping JSON to a file
|
||
|
*
|
||
|
* @param {Gio.File|string} file - A Gio.File or file path
|
||
|
* @param {object} obj - The object to write to disk
|
||
|
* @param {boolean} sync - Default is %false, if %true load synchronously
|
||
|
*/
|
||
|
JSON.dump = function (obj, file, sync = false) {
|
||
|
if (typeof file === 'string') {
|
||
|
file = Gio.File.new_for_path(file);
|
||
|
}
|
||
|
|
||
|
if (sync) {
|
||
|
file.replace_contents(
|
||
|
JSON.stringify(obj, null, 2),
|
||
|
null,
|
||
|
false,
|
||
|
Gio.FileCreateFlags.REPLACE_DESTINATION,
|
||
|
null
|
||
|
);
|
||
|
} else {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
file.replace_contents_bytes_async(
|
||
|
new GLib.Bytes(JSON.stringify(obj, null, 2)),
|
||
|
null,
|
||
|
false,
|
||
|
Gio.FileCreateFlags.REPLACE_DESTINATION,
|
||
|
null,
|
||
|
(file, res) => {
|
||
|
try {
|
||
|
file.replace_contents_finish(res);
|
||
|
resolve();
|
||
|
} catch (e) {
|
||
|
reject(e);
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Idle Promise
|
||
|
*
|
||
|
* @param {number} priority - The priority of the idle source
|
||
|
*/
|
||
|
Promise.idle = function(priority) {
|
||
|
return new Promise(resolve => GLib.idle_add(priority, resolve));
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Timeout Promise
|
||
|
*
|
||
|
* @param {number} priority - The priority of the timeout source
|
||
|
* @param {number} interval - Delay in milliseconds before resolving
|
||
|
*/
|
||
|
Promise.timeout = function(priority = GLib.PRIORITY_DEFAULT, interval = 100) {
|
||
|
return new Promise(resolve => GLib.timeout_add(priority, interval, resolve));
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* A simple (for now) pre-comparison sanitizer for phone numbers
|
||
|
* See: https://github.com/KDE/kdeconnect-kde/blob/master/smsapp/conversationlistmodel.cpp#L200-L210
|
||
|
*
|
||
|
* @return {string} - Return the string stripped of leading 0, and ' ()-+'
|
||
|
*/
|
||
|
String.prototype.toPhoneNumber = function() {
|
||
|
let strippedNumber = this.replace(/^0*|[ ()+-]/g, '');
|
||
|
|
||
|
if (strippedNumber.length)
|
||
|
return strippedNumber;
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* A simple equality check for phone numbers based on `toPhoneNumber()`
|
||
|
*
|
||
|
* @param {string} number - A phone number string to compare
|
||
|
* @return {boolean} - If `this` and @number are equivalent phone numbers
|
||
|
*/
|
||
|
String.prototype.equalsPhoneNumber = function(number) {
|
||
|
let a = this.toPhoneNumber();
|
||
|
let b = number.toPhoneNumber();
|
||
|
|
||
|
return (a.endsWith(b) || b.endsWith(a));
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* An implementation of `rm -rf` in Gio
|
||
|
*/
|
||
|
Gio.File.rm_rf = function(file) {
|
||
|
try {
|
||
|
if (typeof file === 'string') {
|
||
|
file = Gio.File.new_for_path(file);
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
let iter = file.enumerate_children(
|
||
|
'standard::name',
|
||
|
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
|
||
|
null
|
||
|
);
|
||
|
|
||
|
let info;
|
||
|
|
||
|
while ((info = iter.next_file(null))) {
|
||
|
Gio.File.rm_rf(iter.get_child(info));
|
||
|
}
|
||
|
|
||
|
iter.close(null);
|
||
|
} catch (e) {
|
||
|
// Silence errors
|
||
|
}
|
||
|
|
||
|
file.delete(null);
|
||
|
} catch (e) {
|
||
|
// Silence errors
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Extend GLib.Variant with a static method to recursively pack a variant
|
||
|
*
|
||
|
* @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal.
|
||
|
*/
|
||
|
function _full_pack(obj) {
|
||
|
let packed;
|
||
|
let type = typeof obj;
|
||
|
|
||
|
switch (true) {
|
||
|
case (obj instanceof GLib.Variant):
|
||
|
return obj;
|
||
|
|
||
|
case (type === 'string'):
|
||
|
return GLib.Variant.new('s', obj);
|
||
|
|
||
|
case (type === 'number'):
|
||
|
return GLib.Variant.new('d', obj);
|
||
|
|
||
|
case (type === 'boolean'):
|
||
|
return GLib.Variant.new('b', obj);
|
||
|
|
||
|
case (obj instanceof Uint8Array):
|
||
|
return GLib.Variant.new('ay', obj);
|
||
|
|
||
|
case (obj === null):
|
||
|
return GLib.Variant.new('mv', null);
|
||
|
|
||
|
case (typeof obj.map === 'function'):
|
||
|
return GLib.Variant.new(
|
||
|
'av',
|
||
|
obj.filter(e => e !== undefined).map(e => _full_pack(e))
|
||
|
);
|
||
|
|
||
|
case (obj instanceof Gio.Icon):
|
||
|
return obj.serialize();
|
||
|
|
||
|
case (type === 'object'):
|
||
|
packed = {};
|
||
|
|
||
|
for (let [key, val] of Object.entries(obj)) {
|
||
|
if (val !== undefined) {
|
||
|
packed[key] = _full_pack(val);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return GLib.Variant.new('a{sv}', packed);
|
||
|
|
||
|
default:
|
||
|
throw Error(`Unsupported type '${type}': ${obj}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
GLib.Variant.full_pack = _full_pack;
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Extend GLib.Variant with a method to recursively deepUnpack() a variant
|
||
|
*
|
||
|
* TODO: this is duplicated in components/dbus.js and it probably shouldn't be,
|
||
|
* but dbus.js can stand on it's own if it is...
|
||
|
*
|
||
|
* @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal.
|
||
|
*/
|
||
|
function _full_unpack(obj) {
|
||
|
obj = (obj === undefined) ? this : obj;
|
||
|
let unpacked;
|
||
|
|
||
|
switch (true) {
|
||
|
case (obj === null):
|
||
|
return obj;
|
||
|
|
||
|
case (obj instanceof GLib.Variant):
|
||
|
return _full_unpack(obj.deepUnpack());
|
||
|
|
||
|
case (obj instanceof Uint8Array):
|
||
|
return obj;
|
||
|
|
||
|
case (typeof obj.map === 'function'):
|
||
|
return obj.map(e => _full_unpack(e));
|
||
|
|
||
|
case (typeof obj === 'object'):
|
||
|
unpacked = {};
|
||
|
|
||
|
for (let [key, value] of Object.entries(obj)) {
|
||
|
// Try to detect and deserialize GIcons
|
||
|
try {
|
||
|
if (key === 'icon' && value.get_type_string() === '(sv)') {
|
||
|
unpacked[key] = Gio.Icon.deserialize(value);
|
||
|
} else {
|
||
|
unpacked[key] = _full_unpack(value);
|
||
|
}
|
||
|
} catch (e) {
|
||
|
unpacked[key] = _full_unpack(value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return unpacked;
|
||
|
|
||
|
default:
|
||
|
return obj;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
GLib.Variant.prototype.full_unpack = _full_unpack;
|
||
|
|