![Giovanni Campagna](/assets/img/avatar_default.png)
All classes that have at least one other derived class (and thus benefit from the framework) have been now ported. These includes NMDevice, SearchProvider, AltTab.SwitcherList, and some other stuff around. https://bugzilla.gnome.org/show_bug.cgi?id=664436
389 lines
14 KiB
JavaScript
389 lines
14 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
const Gio = imports.gi.Gio;
|
|
const GLib = imports.gi.GLib;
|
|
const Lang = imports.lang;
|
|
const Meta = imports.gi.Meta;
|
|
const St = imports.gi.St;
|
|
const Shell = imports.gi.Shell;
|
|
const Signals = imports.signals;
|
|
|
|
const FileUtils = imports.misc.fileUtils;
|
|
const Main = imports.ui.main;
|
|
const ModalDialog = imports.ui.modalDialog;
|
|
const ShellEntry = imports.ui.shellEntry;
|
|
const Tweener = imports.ui.tweener;
|
|
const Util = imports.misc.util;
|
|
const History = imports.misc.history;
|
|
|
|
const MAX_FILE_DELETED_BEFORE_INVALID = 10;
|
|
|
|
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';
|
|
|
|
const DIALOG_GROW_TIME = 0.1;
|
|
|
|
function CommandCompleter() {
|
|
this._init();
|
|
}
|
|
|
|
CommandCompleter.prototype = {
|
|
_init : function() {
|
|
this._changedCount = 0;
|
|
this._paths = GLib.getenv('PATH').split(':');
|
|
this._paths.push(GLib.get_home_dir());
|
|
this._valid = false;
|
|
this._updateInProgress = false;
|
|
this._childs = new Array(this._paths.length);
|
|
this._monitors = new Array(this._paths.length);
|
|
for (let i = 0; i < this._paths.length; i++) {
|
|
this._childs[i] = [];
|
|
let file = Gio.file_new_for_path(this._paths[i]);
|
|
let info;
|
|
try {
|
|
info = file.query_info(Gio.FILE_ATTRIBUTE_STANDARD_TYPE, Gio.FileQueryInfoFlags.NONE, null);
|
|
} catch (e) {
|
|
// FIXME catchall
|
|
this._paths[i] = null;
|
|
continue;
|
|
}
|
|
|
|
if (info.get_attribute_uint32(Gio.FILE_ATTRIBUTE_STANDARD_TYPE) != Gio.FileType.DIRECTORY)
|
|
continue;
|
|
|
|
this._paths[i] = file.get_path();
|
|
this._monitors[i] = file.monitor_directory(Gio.FileMonitorFlags.NONE, null);
|
|
if (this._monitors[i] != null) {
|
|
this._monitors[i].connect('changed', Lang.bind(this, this._onChanged));
|
|
}
|
|
}
|
|
this._paths = this._paths.filter(function(a) {
|
|
return a != null;
|
|
});
|
|
this._update(0);
|
|
},
|
|
|
|
update : function() {
|
|
if (this._valid)
|
|
return;
|
|
this._update(0);
|
|
},
|
|
|
|
_update : function(i) {
|
|
if (i == 0 && this._updateInProgress)
|
|
return;
|
|
this._updateInProgress = true;
|
|
this._changedCount = 0;
|
|
this._i = i;
|
|
if (i >= this._paths.length) {
|
|
this._valid = true;
|
|
this._updateInProgress = false;
|
|
return;
|
|
}
|
|
let file = Gio.file_new_for_path(this._paths[i]);
|
|
this._childs[this._i] = [];
|
|
FileUtils.listDirAsync(file, Lang.bind(this, function (files) {
|
|
for (let i = 0; i < files.length; i++) {
|
|
this._childs[this._i].push(files[i].get_name());
|
|
}
|
|
this._update(this._i + 1);
|
|
}));
|
|
},
|
|
|
|
_onChanged : function(m, f, of, type) {
|
|
if (!this._valid)
|
|
return;
|
|
let path = f.get_parent().get_path();
|
|
let k = undefined;
|
|
for (let i = 0; i < this._paths.length; i++) {
|
|
if (this._paths[i] == path)
|
|
k = i;
|
|
}
|
|
if (k === undefined) {
|
|
return;
|
|
}
|
|
if (type == Gio.FileMonitorEvent.CREATED) {
|
|
this._childs[k].push(f.get_basename());
|
|
}
|
|
if (type == Gio.FileMonitorEvent.DELETED) {
|
|
this._changedCount++;
|
|
if (this._changedCount > MAX_FILE_DELETED_BEFORE_INVALID) {
|
|
this._valid = false;
|
|
}
|
|
let name = f.get_basename();
|
|
this._childs[k] = this._childs[k].filter(function(e) {
|
|
return e != name;
|
|
});
|
|
}
|
|
if (type == Gio.FileMonitorEvent.UNMOUNTED) {
|
|
this._childs[k] = [];
|
|
}
|
|
},
|
|
|
|
getCompletion: function(text) {
|
|
let common = '';
|
|
let notInit = true;
|
|
if (!this._valid) {
|
|
this._update(0);
|
|
return common;
|
|
}
|
|
function _getCommon(s1, 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);
|
|
}
|
|
function _hasPrefix(s1, prefix) {
|
|
return s1.indexOf(prefix) == 0;
|
|
}
|
|
for (let i = 0; i < this._childs.length; i++) {
|
|
for (let k = 0; k < this._childs[i].length; k++) {
|
|
if (!_hasPrefix(this._childs[i][k], text))
|
|
continue;
|
|
if (notInit) {
|
|
common = this._childs[i][k];
|
|
notInit = false;
|
|
}
|
|
common = _getCommon(common, this._childs[i][k]);
|
|
}
|
|
}
|
|
if (common.length)
|
|
return common.substr(text.length);
|
|
return common;
|
|
}
|
|
};
|
|
|
|
const RunDialog = new Lang.Class({
|
|
Name: 'RunDialog',
|
|
Extends: ModalDialog.ModalDialog,
|
|
|
|
_init : function() {
|
|
this.parent({ styleClass: 'run-dialog' });
|
|
|
|
this._lockdownSettings = new Gio.Settings({ schema: LOCKDOWN_SCHEMA });
|
|
this._terminalSettings = new Gio.Settings({ schema: TERMINAL_SCHEMA });
|
|
global.settings.connect('changed::development-tools', Lang.bind(this, function () {
|
|
this._enableInternalCommands = global.settings.get_boolean('development-tools');
|
|
}));
|
|
this._enableInternalCommands = global.settings.get_boolean('development-tools');
|
|
|
|
this._internalCommands = { 'lg':
|
|
Lang.bind(this, function() {
|
|
Main.createLookingGlass().open();
|
|
}),
|
|
|
|
'r': Lang.bind(this, function() {
|
|
global.reexec_self();
|
|
}),
|
|
|
|
// Developer brain backwards compatibility
|
|
'restart': Lang.bind(this, function() {
|
|
global.reexec_self();
|
|
}),
|
|
|
|
'debugexit': Lang.bind(this, function() {
|
|
Meta.quit(Meta.ExitCode.ERROR);
|
|
}),
|
|
|
|
// rt is short for "reload theme"
|
|
'rt': Lang.bind(this, function() {
|
|
Main.loadTheme();
|
|
})
|
|
};
|
|
|
|
|
|
let label = new St.Label({ style_class: 'run-dialog-label',
|
|
text: _("Please enter a command:") });
|
|
|
|
this.contentLayout.add(label, { y_align: St.Align.START });
|
|
|
|
let entry = new St.Entry({ style_class: 'run-dialog-entry' });
|
|
ShellEntry.addContextMenu(entry);
|
|
|
|
this._entryText = entry.clutter_text;
|
|
this.contentLayout.add(entry, { y_align: St.Align.START });
|
|
this.setInitialKeyFocus(this._entryText);
|
|
|
|
this._errorBox = new St.BoxLayout({ style_class: 'run-dialog-error-box' });
|
|
|
|
this.contentLayout.add(this._errorBox, { expand: true });
|
|
|
|
let errorIcon = new St.Icon({ icon_name: 'dialog-error', icon_size: 24, style_class: 'run-dialog-error-icon' });
|
|
|
|
this._errorBox.add(errorIcon, { y_align: St.Align.MIDDLE });
|
|
|
|
this._commandError = false;
|
|
|
|
this._errorMessage = new St.Label({ style_class: 'run-dialog-error-label' });
|
|
this._errorMessage.clutter_text.line_wrap = true;
|
|
|
|
this._errorBox.add(this._errorMessage, { expand: true,
|
|
y_align: St.Align.MIDDLE,
|
|
y_fill: false });
|
|
|
|
this._errorBox.hide();
|
|
|
|
this._pathCompleter = new Gio.FilenameCompleter();
|
|
this._commandCompleter = new CommandCompleter();
|
|
this._group.connect('notify::visible', Lang.bind(this._commandCompleter, this._commandCompleter.update));
|
|
|
|
this._history = new History.HistoryManager({ gsettingsKey: HISTORY_KEY,
|
|
entry: this._entryText });
|
|
this._entryText.connect('key-press-event', Lang.bind(this, function(o, e) {
|
|
let symbol = e.get_key_symbol();
|
|
if (symbol == Clutter.Return || symbol == Clutter.KP_Enter) {
|
|
this.popModal();
|
|
if (Shell.get_event_state(e) & Clutter.ModifierType.CONTROL_MASK)
|
|
this._run(o.get_text(), true);
|
|
else
|
|
this._run(o.get_text(), false);
|
|
if (!this._commandError)
|
|
this.close();
|
|
else {
|
|
if (!this.pushModal())
|
|
this.close();
|
|
}
|
|
return true;
|
|
}
|
|
if (symbol == Clutter.Escape) {
|
|
this.close();
|
|
return true;
|
|
}
|
|
if (symbol == Clutter.slash) {
|
|
// Need preload data before get completion. GFilenameCompleter load content of parent directory.
|
|
// Parent directory for /usr/include/ is /usr/. So need to add fake name('a').
|
|
let text = o.get_text().concat('/a');
|
|
let prefix;
|
|
if (text.lastIndexOf(' ') == -1)
|
|
prefix = text;
|
|
else
|
|
prefix = text.substr(text.lastIndexOf(' ') + 1);
|
|
this._getCompletion(prefix);
|
|
return false;
|
|
}
|
|
if (symbol == Clutter.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);
|
|
if (postfix[postfix.length - 1] == '/')
|
|
this._getCompletion(text + postfix + 'a');
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}));
|
|
},
|
|
|
|
_getCompletion : function(text) {
|
|
if (text.indexOf('/') != -1) {
|
|
return this._pathCompleter.get_completion_suffix(text);
|
|
} else {
|
|
return this._commandCompleter.getCompletion(text);
|
|
}
|
|
},
|
|
|
|
_run : function(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 exec_arg = this._terminalSettings.get_string(EXEC_ARG_KEY);
|
|
command = exec + ' ' + exec_arg + ' ' + 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 = 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());
|
|
} catch (e) {
|
|
// 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 = e.message.replace(/[^:]*: *(.+)/, '$1');
|
|
this._showError(message);
|
|
}
|
|
} else {
|
|
this._showError(e.message);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
_showError : function(message) {
|
|
this._commandError = true;
|
|
|
|
this._errorMessage.set_text(message);
|
|
|
|
if (!this._errorBox.visible) {
|
|
let [errorBoxMinHeight, errorBoxNaturalHeight] = this._errorBox.get_preferred_height(-1);
|
|
|
|
let parentActor = this._errorBox.get_parent();
|
|
Tweener.addTween(parentActor,
|
|
{ height: parentActor.height + errorBoxNaturalHeight,
|
|
time: DIALOG_GROW_TIME,
|
|
transition: 'easeOutQuad',
|
|
onComplete: Lang.bind(this,
|
|
function() {
|
|
parentActor.set_height(-1);
|
|
this._errorBox.show();
|
|
})
|
|
});
|
|
}
|
|
},
|
|
|
|
open: function() {
|
|
this._history.lastItem();
|
|
this._errorBox.hide();
|
|
this._entryText.set_text('');
|
|
this._commandError = false;
|
|
|
|
if (this._lockdownSettings.get_boolean(DISABLE_COMMAND_LINE_KEY))
|
|
return;
|
|
|
|
this.parent();
|
|
},
|
|
});
|
|
Signals.addSignalMethods(RunDialog.prototype);
|