9d941f8202
xgettext gained some support for template strings, and no longer fails when encountering '/' somewhere between backticks. Unfortunately its support is still buggy as hell, and it is now silently dropping translatable strings, yay. I hate making the code worse, but until xgettext really gets its shit together, the only viable way forward seems to be to not use template strings in any files listed in POTFILES. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/1014
257 lines
8.5 KiB
JavaScript
257 lines
8.5 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
/* exported RunDialog */
|
|
|
|
const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
|
|
|
|
const Dialog = imports.ui.dialog;
|
|
const Main = imports.ui.main;
|
|
const ModalDialog = imports.ui.modalDialog;
|
|
const ShellEntry = imports.ui.shellEntry;
|
|
const Util = imports.misc.util;
|
|
const History = imports.misc.history;
|
|
|
|
const HISTORY_KEY = 'command-history';
|
|
|
|
const LOCKDOWN_SCHEMA = 'org.gnome.desktop.lockdown';
|
|
const DISABLE_COMMAND_LINE_KEY = 'disable-command-line';
|
|
|
|
const TERMINAL_SCHEMA = 'org.gnome.desktop.default-applications.terminal';
|
|
const EXEC_KEY = 'exec';
|
|
const EXEC_ARG_KEY = 'exec-arg';
|
|
|
|
var RunDialog = GObject.registerClass(
|
|
class RunDialog extends ModalDialog.ModalDialog {
|
|
_init() {
|
|
super._init({
|
|
styleClass: 'run-dialog',
|
|
destroyOnClose: false,
|
|
});
|
|
|
|
this._lockdownSettings = new Gio.Settings({ schema_id: LOCKDOWN_SCHEMA });
|
|
this._terminalSettings = new Gio.Settings({ schema_id: TERMINAL_SCHEMA });
|
|
global.settings.connect('changed::development-tools', () => {
|
|
this._enableInternalCommands = global.settings.get_boolean('development-tools');
|
|
});
|
|
this._enableInternalCommands = global.settings.get_boolean('development-tools');
|
|
|
|
this._internalCommands = {
|
|
'lg': () => Main.createLookingGlass().open(),
|
|
|
|
'r': this._restart.bind(this),
|
|
|
|
// Developer brain backwards compatibility
|
|
'restart': this._restart.bind(this),
|
|
|
|
'debugexit': () => Meta.quit(Meta.ExitCode.ERROR),
|
|
|
|
// rt is short for "reload theme"
|
|
'rt': () => {
|
|
Main.reloadThemeResource();
|
|
Main.loadTheme();
|
|
},
|
|
|
|
'check_cloexec_fds': () => {
|
|
Shell.util_check_cloexec_fds();
|
|
},
|
|
};
|
|
|
|
let title = _('Run a Command');
|
|
|
|
let content = new Dialog.MessageDialogContent({ title });
|
|
this.contentLayout.add_actor(content);
|
|
|
|
let entry = new St.Entry({
|
|
style_class: 'run-dialog-entry',
|
|
can_focus: true,
|
|
});
|
|
ShellEntry.addContextMenu(entry);
|
|
|
|
this._entryText = entry.clutter_text;
|
|
content.add_child(entry);
|
|
this.setInitialKeyFocus(this._entryText);
|
|
|
|
let defaultDescriptionText = _('Press ESC to close');
|
|
|
|
this._descriptionLabel = new St.Label({
|
|
style_class: 'run-dialog-description',
|
|
text: defaultDescriptionText,
|
|
});
|
|
content.add_child(this._descriptionLabel);
|
|
|
|
this._commandError = false;
|
|
|
|
this._pathCompleter = new Gio.FilenameCompleter();
|
|
|
|
this._history = new History.HistoryManager({
|
|
gsettingsKey: HISTORY_KEY,
|
|
entry: this._entryText,
|
|
});
|
|
this._entryText.connect('activate', o => {
|
|
this.popModal();
|
|
this._run(o.get_text(),
|
|
Clutter.get_current_event().get_state() & Clutter.ModifierType.CONTROL_MASK);
|
|
if (!this._commandError ||
|
|
!this.pushModal())
|
|
this.close();
|
|
});
|
|
this._entryText.connect('key-press-event', (o, e) => {
|
|
let symbol = e.get_key_symbol();
|
|
if (symbol === Clutter.KEY_Tab) {
|
|
let text = o.get_text();
|
|
let prefix;
|
|
if (text.lastIndexOf(' ') == -1)
|
|
prefix = text;
|
|
else
|
|
prefix = text.substr(text.lastIndexOf(' ') + 1);
|
|
let postfix = this._getCompletion(prefix);
|
|
if (postfix != null && postfix.length > 0) {
|
|
o.insert_text(postfix, -1);
|
|
o.set_cursor_position(text.length + postfix.length);
|
|
}
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
return Clutter.EVENT_PROPAGATE;
|
|
});
|
|
this._entryText.connect('text-changed', () => {
|
|
this._descriptionLabel.set_text(defaultDescriptionText);
|
|
});
|
|
}
|
|
|
|
vfunc_key_release_event(event) {
|
|
if (event.keyval === Clutter.KEY_Escape) {
|
|
this.close();
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
|
|
return Clutter.EVENT_PROPAGATE;
|
|
}
|
|
|
|
_getCommandCompletion(text) {
|
|
function _getCommon(s1, s2) {
|
|
if (s1 == null)
|
|
return s2;
|
|
|
|
let k = 0;
|
|
for (; k < s1.length && k < s2.length; k++) {
|
|
if (s1[k] != s2[k])
|
|
break;
|
|
}
|
|
if (k == 0)
|
|
return '';
|
|
return s1.substr(0, k);
|
|
}
|
|
|
|
let paths = GLib.getenv('PATH').split(':');
|
|
paths.push(GLib.get_home_dir());
|
|
let someResults = paths.map(path => {
|
|
let results = [];
|
|
try {
|
|
let file = Gio.File.new_for_path(path);
|
|
let fileEnum = file.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
|
|
let info;
|
|
while ((info = fileEnum.next_file(null))) {
|
|
let name = info.get_name();
|
|
if (name.slice(0, text.length) == text)
|
|
results.push(name);
|
|
}
|
|
} catch (e) {
|
|
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND) &&
|
|
!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_DIRECTORY))
|
|
log(e);
|
|
}
|
|
return results;
|
|
});
|
|
let results = someResults.reduce((a, b) => a.concat(b), []);
|
|
|
|
if (!results.length)
|
|
return null;
|
|
|
|
let common = results.reduce(_getCommon, null);
|
|
return common.substr(text.length);
|
|
}
|
|
|
|
_getCompletion(text) {
|
|
if (text.includes('/'))
|
|
return this._pathCompleter.get_completion_suffix(text);
|
|
else
|
|
return this._getCommandCompletion(text);
|
|
}
|
|
|
|
_run(input, inTerminal) {
|
|
let command = input;
|
|
|
|
this._history.addItem(input);
|
|
this._commandError = false;
|
|
let f;
|
|
if (this._enableInternalCommands)
|
|
f = this._internalCommands[input];
|
|
else
|
|
f = null;
|
|
if (f) {
|
|
f();
|
|
} else if (input) {
|
|
try {
|
|
if (inTerminal) {
|
|
let exec = this._terminalSettings.get_string(EXEC_KEY);
|
|
let execArg = this._terminalSettings.get_string(EXEC_ARG_KEY);
|
|
command = '%s %s %s'.format(exec, execArg, input);
|
|
}
|
|
Util.trySpawnCommandLine(command);
|
|
} catch (e) {
|
|
// Mmmh, that failed - see if @input matches an existing file
|
|
let path = null;
|
|
if (input.charAt(0) == '/') {
|
|
path = input;
|
|
} else {
|
|
if (input.charAt(0) == '~')
|
|
input = input.slice(1);
|
|
path = '%s/%s'.format(GLib.get_home_dir(), input);
|
|
}
|
|
|
|
if (GLib.file_test(path, GLib.FileTest.EXISTS)) {
|
|
let file = Gio.file_new_for_path(path);
|
|
try {
|
|
Gio.app_info_launch_default_for_uri(file.get_uri(),
|
|
global.create_app_launch_context(0, -1));
|
|
} catch (err) {
|
|
// The exception from gjs contains an error string like:
|
|
// Error invoking Gio.app_info_launch_default_for_uri: No application
|
|
// is registered as handling this file
|
|
// We are only interested in the part after the first colon.
|
|
let message = err.message.replace(/[^:]*: *(.+)/, '$1');
|
|
this._showError(message);
|
|
}
|
|
} else {
|
|
this._showError(e.message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_showError(message) {
|
|
this._commandError = true;
|
|
this._descriptionLabel.set_text(message);
|
|
}
|
|
|
|
_restart() {
|
|
if (Meta.is_wayland_compositor()) {
|
|
this._showError(_("Restart is not available on Wayland"));
|
|
return;
|
|
}
|
|
this._shouldFadeOut = false;
|
|
this.close();
|
|
Meta.restart(_("Restarting…"));
|
|
}
|
|
|
|
open() {
|
|
this._history.lastItem();
|
|
this._entryText.set_text('');
|
|
this._commandError = false;
|
|
|
|
if (this._lockdownSettings.get_boolean(DISABLE_COMMAND_LINE_KEY))
|
|
return;
|
|
|
|
super.open();
|
|
}
|
|
});
|