gnome-shell/js/misc/util.js

456 lines
13 KiB
JavaScript
Raw Normal View History

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Shell from 'gi://Shell';
import St from 'gi://St';
import GnomeDesktop from 'gi://GnomeDesktop';
import * as Main from '../ui/main.js';
import {formatTime} from './dateUtils.js';
// 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(
`(^|${_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
`${_balancedParens}` + // balanced parens
')+' +
'(?:' + // end with:
`${_balancedParens}` + // balanced parens
'|' + // or
`${_notTrailingJunk}` + // last non-junk char
')' +
')', 'gi');
let _desktopSettings = null;
/**
* findUrls:
*
* @param {string} 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.
*
* @returns {{url: string, pos: number}[]} the list of match objects, as described above
*/
export 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:
*
* Runs `argv` in the background, handling any errors that occur
* when trying to start the program.
*
* @param {readonly string[]} argv an argv array
*/
export function spawn(argv) {
try {
trySpawn(argv);
} catch (err) {
_handleSpawnError(argv[0], err);
}
}
/**
* spawnCommandLine:
*
* @param {readonly string[]} commandLine a command line
*
* Runs commandLine in the background, handling any errors that
* occur when trying to parse or start the program.
*/
export function spawnCommandLine(commandLine) {
try {
let [success_, argv] = GLib.shell_parse_argv(commandLine);
trySpawn(argv);
} catch (err) {
_handleSpawnError(commandLine, err);
}
}
/**
* spawnApp:
*
* @param {readonly string[]} argv an argv array
*
* Runs argv as if it was an application, handling startup notification
*/
export function spawnApp(argv) {
try {
const 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:
*
* @param {readonly string[]} argv an argv array
*
* Runs argv in the background. If launching argv fails,
* this will throw an error.
*/
export function trySpawn(argv) {
let success_, pid;
try {
[success_, pid] = GLib.spawn_async(
null, argv, null,
GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD,
() => {
try {
global.context.restore_rlimit_nofile();
} catch (err) {
}
}
);
} 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:
*
* @param {readonly string[]} commandLine a command line
*
* Runs commandLine in the background. If launching commandLine
* fails, this will throw an error.
*/
export 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(/[^:]*: /, `${_('Could not parse command:')}\n`);
throw err;
}
trySpawn(argv);
}
function _handleSpawnError(command, err) {
let title = _('Execution of “%s” failed:').format(command);
Main.notifyError(title, err.message);
}
/**
* Returns an {@link St.Label} with the date passed formatted
* using {@link formatTime}
*
* @param {Date} date the date to format for the label
* @param {object} params params for {@link formatTime}
* @returns {St.Label}
*/
export 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)});
_desktopSettings.connectObject(
'changed::clock-format', () => (label.text = formatTime(date, params)),
label);
return label;
}
/**
* lowerBound:
*
* @template T, [K=T]
* @param {readonly T[]} array an array or array-like object, already sorted
* according to `cmp`
* @param {K} val the value to add
* @param {(a: T, val: K) => number} cmp a comparator (or undefined to compare as numbers)
* @returns {number}
*
* 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 ||= (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:
*
* @template T, [K=T]
* @param {T[]} array an array sorted according to `cmp`
* @param {K} val a value to insert
* @param {(a: T, val: K) => number} cmp the sorting function
* @returns {number}
*
* Inserts `val` into `array`, preserving the
* sorting invariants.
*
* Returns the position at which it was inserted
*/
export function insertSorted(array, val, cmp) {
let pos = lowerBound(array, val, cmp);
array.splice(pos, 0, val);
return pos;
}
/**
* @param {number} start
* @param {number} end
* @param {number} progress
* @returns {number}
*/
export function lerp(start, end, progress) {
return start + progress * (end - start);
}
/**
* _GNOMEversionToNumber:
*
* @param {string} version a GNOME version element
* @returns {number}
*
* Like Number() but returns sortable values for special-cases
* 'alpha' and 'beta'. Returns NaN for unhandled 'versions'.
*/
function _GNOMEversionToNumber(version) {
let ret = Number(version);
if (!isNaN(ret))
return ret;
if (version === 'alpha')
return -2;
if (version === 'beta')
return -1;
return ret;
}
/**
* GNOMEversionCompare:
*
* @param {string} version1 a string containing a GNOME version
* @param {string} version2 a string containing another GNOME version
* @returns {number}
*
* Returns an integer less than, equal to, or greater than
* zero, if `version1` is older, equal or newer than `version2`
*/
export function GNOMEversionCompare(version1, version2) {
const v1Array = version1.split('.');
const v2Array = version2.split('.');
for (let i = 0; i < Math.max(v1Array.length, v2Array.length); i++) {
let elemV1 = _GNOMEversionToNumber(v1Array[i] || '0');
let elemV2 = _GNOMEversionToNumber(v2Array[i] || '0');
if (elemV1 < elemV2)
return -1;
if (elemV1 > elemV2)
return 1;
}
return 0;
}
export class DBusSenderChecker {
/**
* @param {string[]} allowList - list of allowed well-known names
*/
constructor(allowList) {
this._allowlistMap = new Map();
this._uninitializedNames = new Set(allowList);
this._initializedPromise = new Promise(resolve => {
this._resolveInitialized = resolve;
});
this._watchList = allowList.map(name => {
return Gio.DBus.watch_name(Gio.BusType.SESSION,
name,
Gio.BusNameWatcherFlags.NONE,
(conn_, name_, owner) => {
this._allowlistMap.set(name, owner);
this._checkAndResolveInitialized(name);
},
() => {
this._allowlistMap.delete(name);
this._checkAndResolveInitialized(name);
});
});
}
/**
* @param {string} name - bus name for which the watcher got initialized
*/
_checkAndResolveInitialized(name) {
if (this._uninitializedNames.delete(name) &&
this._uninitializedNames.size === 0)
this._resolveInitialized();
}
/**
* @async
* @param {string} sender - the bus name that invoked the checked method
* @returns {bool}
*/
async _isSenderAllowed(sender) {
await this._initializedPromise;
return [...this._allowlistMap.values()].includes(sender);
}
/**
* Check whether the bus name that invoked @invocation maps
* to an entry in the allow list.
*
* @async
* @throws
* @param {Gio.DBusMethodInvocation} invocation - the invocation
* @returns {void}
*/
async checkInvocation(invocation) {
if (global.context.unsafe_mode)
return;
if (await this._isSenderAllowed(invocation.get_sender()))
return;
throw new GLib.Error(Gio.DBusError,
Gio.DBusError.ACCESS_DENIED,
`${invocation.get_method_name()} is not allowed`);
}
/**
* @returns {void}
*/
destroy() {
for (const id in this._watchList)
Gio.DBus.unwatch_name(id);
this._watchList = [];
}
}
/* @class Highlighter Highlight given terms in text using markup. */
export class Highlighter {
/**
* @param {?string[]} terms - list of terms to highlight
*/
constructor(terms) {
if (!terms)
return;
const escapedTerms = terms
.map(term => Shell.util_regex_escape(term))
.filter(term => term.length > 0);
if (escapedTerms.length === 0)
return;
this._highlightRegex = new RegExp(
`(${escapedTerms.join('|')})`, 'gi');
}
/**
* Highlight all occurences of the terms defined for this
* highlighter in the provided text using markup.
*
* @param {string} text - text to highlight the defined terms in
* @returns {string}
*/
highlight(text) {
if (!this._highlightRegex)
return GLib.markup_escape_text(text, -1);
let escaped = [];
let lastMatchEnd = 0;
let match;
while ((match = this._highlightRegex.exec(text))) {
if (match.index > lastMatchEnd) {
let unmatched = GLib.markup_escape_text(
text.slice(lastMatchEnd, match.index), -1);
escaped.push(unmatched);
}
let matched = GLib.markup_escape_text(match[0], -1);
escaped.push(`<b>${matched}</b>`);
lastMatchEnd = match.index + match[0].length;
}
let unmatched = GLib.markup_escape_text(
text.slice(lastMatchEnd), -1);
escaped.push(unmatched);
return escaped.join('');
}
}