2505 lines
111 KiB
JavaScript
2505 lines
111 KiB
JavaScript
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Mozilla Skywriter.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2009
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Kevin Dangoor (kdangoor@mozilla.com)
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
define('cockpit/index', ['require', 'exports', 'module' , 'pilot/index', 'cockpit/cli', 'cockpit/ui/settings', 'cockpit/ui/cli_view', 'cockpit/commands/basic'], function(require, exports, module) {
|
|
|
|
|
|
exports.startup = function(data, reason) {
|
|
require('pilot/index');
|
|
require('cockpit/cli').startup(data, reason);
|
|
// window.testCli = require('cockpit/test/testCli');
|
|
|
|
require('cockpit/ui/settings').startup(data, reason);
|
|
require('cockpit/ui/cli_view').startup(data, reason);
|
|
require('cockpit/commands/basic').startup(data, reason);
|
|
};
|
|
|
|
/*
|
|
exports.shutdown(data, reason) {
|
|
};
|
|
*/
|
|
|
|
|
|
});
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Skywriter.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2009
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Joe Walker (jwalker@mozilla.com)
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
define('cockpit/cli', ['require', 'exports', 'module' , 'pilot/console', 'pilot/lang', 'pilot/oop', 'pilot/event_emitter', 'pilot/types', 'pilot/canon'], function(require, exports, module) {
|
|
|
|
|
|
var console = require('pilot/console');
|
|
var lang = require('pilot/lang');
|
|
var oop = require('pilot/oop');
|
|
var EventEmitter = require('pilot/event_emitter').EventEmitter;
|
|
|
|
//var keyboard = require('keyboard/keyboard');
|
|
var types = require('pilot/types');
|
|
var Status = require('pilot/types').Status;
|
|
var Conversion = require('pilot/types').Conversion;
|
|
var canon = require('pilot/canon');
|
|
|
|
/**
|
|
* Normally type upgrade is done when the owning command is registered, but
|
|
* out commandParam isn't part of a command, so it misses out.
|
|
*/
|
|
exports.startup = function(data, reason) {
|
|
canon.upgradeType('command', commandParam);
|
|
};
|
|
|
|
/**
|
|
* The information required to tell the user there is a problem with their
|
|
* input.
|
|
* TODO: There a several places where {start,end} crop up. Perhaps we should
|
|
* have a Cursor object.
|
|
*/
|
|
function Hint(status, message, start, end, predictions) {
|
|
this.status = status;
|
|
this.message = message;
|
|
|
|
if (typeof start === 'number') {
|
|
this.start = start;
|
|
this.end = end;
|
|
this.predictions = predictions;
|
|
}
|
|
else {
|
|
var arg = start;
|
|
this.start = arg.start;
|
|
this.end = arg.end;
|
|
this.predictions = arg.predictions;
|
|
}
|
|
}
|
|
Hint.prototype = {
|
|
};
|
|
/**
|
|
* Loop over the array of hints finding the one we should display.
|
|
* @param hints array of hints
|
|
*/
|
|
Hint.sort = function(hints, cursor) {
|
|
// Calculate 'distance from cursor'
|
|
if (cursor !== undefined) {
|
|
hints.forEach(function(hint) {
|
|
if (hint.start === Argument.AT_CURSOR) {
|
|
hint.distance = 0;
|
|
}
|
|
else if (cursor < hint.start) {
|
|
hint.distance = hint.start - cursor;
|
|
}
|
|
else if (cursor > hint.end) {
|
|
hint.distance = cursor - hint.end;
|
|
}
|
|
else {
|
|
hint.distance = 0;
|
|
}
|
|
}, this);
|
|
}
|
|
// Sort
|
|
hints.sort(function(hint1, hint2) {
|
|
// Compare first based on distance from cursor
|
|
if (cursor !== undefined) {
|
|
var diff = hint1.distance - hint2.distance;
|
|
if (diff != 0) {
|
|
return diff;
|
|
}
|
|
}
|
|
// otherwise go with hint severity
|
|
return hint2.status - hint1.status;
|
|
});
|
|
// tidy-up
|
|
if (cursor !== undefined) {
|
|
hints.forEach(function(hint) {
|
|
delete hint.distance;
|
|
}, this);
|
|
}
|
|
return hints;
|
|
};
|
|
exports.Hint = Hint;
|
|
|
|
/**
|
|
* A Hint that arose as a result of a Conversion
|
|
*/
|
|
function ConversionHint(conversion, arg) {
|
|
this.status = conversion.status;
|
|
this.message = conversion.message;
|
|
if (arg) {
|
|
this.start = arg.start;
|
|
this.end = arg.end;
|
|
}
|
|
else {
|
|
this.start = 0;
|
|
this.end = 0;
|
|
}
|
|
this.predictions = conversion.predictions;
|
|
};
|
|
oop.inherits(ConversionHint, Hint);
|
|
|
|
|
|
/**
|
|
* We record where in the input string an argument comes so we can report errors
|
|
* against those string positions.
|
|
* We publish a 'change' event when-ever the text changes
|
|
* @param emitter Arguments use something else to pass on change events.
|
|
* Currently this will be the creating Requisition. This prevents dependency
|
|
* loops and prevents us from needing to merge listener lists.
|
|
* @param text The string (trimmed) that contains the argument
|
|
* @param start The position of the text in the original input string
|
|
* @param end See start
|
|
* @param prefix Knowledge of quotation marks and whitespace used prior to the
|
|
* text in the input string allows us to re-generate the original input from
|
|
* the arguments.
|
|
* @param suffix Any quotation marks and whitespace used after the text.
|
|
* Whitespace is normally placed in the prefix to the succeeding argument, but
|
|
* can be used here when this is the last argument.
|
|
* @constructor
|
|
*/
|
|
function Argument(emitter, text, start, end, prefix, suffix) {
|
|
this.emitter = emitter;
|
|
this.setText(text);
|
|
this.start = start;
|
|
this.end = end;
|
|
this.prefix = prefix;
|
|
this.suffix = suffix;
|
|
}
|
|
Argument.prototype = {
|
|
/**
|
|
* Return the result of merging these arguments.
|
|
* TODO: What happens when we're merging arguments for the single string
|
|
* case and some of the arguments are in quotation marks?
|
|
*/
|
|
merge: function(following) {
|
|
if (following.emitter != this.emitter) {
|
|
throw new Error('Can\'t merge Arguments from different EventEmitters');
|
|
}
|
|
return new Argument(
|
|
this.emitter,
|
|
this.text + this.suffix + following.prefix + following.text,
|
|
this.start, following.end,
|
|
this.prefix,
|
|
following.suffix);
|
|
},
|
|
|
|
/**
|
|
* See notes on events in Assignment. We might need to hook changes here
|
|
* into a CliRequisition so they appear of the command line.
|
|
*/
|
|
setText: function(text) {
|
|
if (text == null) {
|
|
throw new Error('Illegal text for Argument: ' + text);
|
|
}
|
|
var ev = { argument: this, oldText: this.text, text: text };
|
|
this.text = text;
|
|
this.emitter._dispatchEvent('argumentChange', ev);
|
|
},
|
|
|
|
/**
|
|
* Helper when we're putting arguments back together
|
|
*/
|
|
toString: function() {
|
|
// TODO: There is a bug here - we should re-escape escaped characters
|
|
// But can we do that reliably?
|
|
return this.prefix + this.text + this.suffix;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Merge an array of arguments into a single argument.
|
|
* All Arguments in the array are expected to have the same emitter
|
|
*/
|
|
Argument.merge = function(argArray, start, end) {
|
|
start = (start === undefined) ? 0 : start;
|
|
end = (end === undefined) ? argArray.length : end;
|
|
|
|
var joined;
|
|
for (var i = start; i < end; i++) {
|
|
var arg = argArray[i];
|
|
if (!joined) {
|
|
joined = arg;
|
|
}
|
|
else {
|
|
joined = joined.merge(arg);
|
|
}
|
|
}
|
|
return joined;
|
|
};
|
|
|
|
/**
|
|
* We sometimes need a way to say 'this error occurs where ever the cursor is'
|
|
*/
|
|
Argument.AT_CURSOR = -1;
|
|
|
|
|
|
/**
|
|
* A link between a parameter and the data for that parameter.
|
|
* The data for the parameter is available as in the preferred type and as
|
|
* an Argument for the CLI.
|
|
* <p>We also record validity information where applicable.
|
|
* <p>For values, null and undefined have distinct definitions. null means
|
|
* that a value has been provided, undefined means that it has not.
|
|
* Thus, null is a valid default value, and common because it identifies an
|
|
* parameter that is optional. undefined means there is no value from
|
|
* the command line.
|
|
* @constructor
|
|
*/
|
|
function Assignment(param, requisition) {
|
|
this.param = param;
|
|
this.requisition = requisition;
|
|
this.setValue(param.defaultValue);
|
|
};
|
|
Assignment.prototype = {
|
|
/**
|
|
* The parameter that we are assigning to
|
|
* @readonly
|
|
*/
|
|
param: undefined,
|
|
|
|
/**
|
|
* Report on the status of the last parse() conversion.
|
|
* @see types.Conversion
|
|
*/
|
|
conversion: undefined,
|
|
|
|
/**
|
|
* The current value in a type as specified by param.type
|
|
*/
|
|
value: undefined,
|
|
|
|
/**
|
|
* The string version of the current value
|
|
*/
|
|
arg: undefined,
|
|
|
|
/**
|
|
* The current value (i.e. not the string representation)
|
|
* Use setValue() to mutate
|
|
*/
|
|
value: undefined,
|
|
setValue: function(value) {
|
|
if (this.value === value) {
|
|
return;
|
|
}
|
|
|
|
if (value === undefined) {
|
|
this.value = this.param.defaultValue;
|
|
this.conversion = this.param.getDefault ?
|
|
this.param.getDefault() :
|
|
this.param.type.getDefault();
|
|
this.arg = undefined;
|
|
} else {
|
|
this.value = value;
|
|
this.conversion = undefined;
|
|
var text = (value == null) ? '' : this.param.type.stringify(value);
|
|
if (this.arg) {
|
|
this.arg.setText(text);
|
|
}
|
|
}
|
|
|
|
this.requisition._assignmentChanged(this);
|
|
},
|
|
|
|
/**
|
|
* The textual representation of the current value
|
|
* Use setValue() to mutate
|
|
*/
|
|
arg: undefined,
|
|
setArgument: function(arg) {
|
|
if (this.arg === arg) {
|
|
return;
|
|
}
|
|
this.arg = arg;
|
|
this.conversion = this.param.type.parse(arg.text);
|
|
this.conversion.arg = arg; // TODO: make this automatic?
|
|
this.value = this.conversion.value;
|
|
this.requisition._assignmentChanged(this);
|
|
},
|
|
|
|
/**
|
|
* Create a list of the hints associated with this parameter assignment.
|
|
* Generally there will be only one hint generated because we're currently
|
|
* only displaying one hint at a time, ordering by distance from cursor
|
|
* and severity. Since distance from cursor will be the same for all hints
|
|
* from this assignment all but the most severe will ever be used. It might
|
|
* make sense with more experience to alter this to function to be getHint()
|
|
*/
|
|
getHint: function() {
|
|
// Allow the parameter to provide documentation
|
|
if (this.param.getCustomHint && this.value && this.arg) {
|
|
var hint = this.param.getCustomHint(this.value, this.arg);
|
|
if (hint) {
|
|
return hint;
|
|
}
|
|
}
|
|
|
|
// If there is no argument, use the cursor position
|
|
var message = '<strong>' + this.param.name + '</strong>: ';
|
|
if (this.param.description) {
|
|
// TODO: This should be a short description - do we need to trim?
|
|
message += this.param.description.trim();
|
|
|
|
// Ensure the help text ends with '. '
|
|
if (message.charAt(message.length - 1) !== '.') {
|
|
message += '.';
|
|
}
|
|
if (message.charAt(message.length - 1) !== ' ') {
|
|
message += ' ';
|
|
}
|
|
}
|
|
var status = Status.VALID;
|
|
var start = this.arg ? this.arg.start : Argument.AT_CURSOR;
|
|
var end = this.arg ? this.arg.end : Argument.AT_CURSOR;
|
|
var predictions;
|
|
|
|
// Non-valid conversions will have useful information to pass on
|
|
if (this.conversion) {
|
|
status = this.conversion.status;
|
|
if (this.conversion.message) {
|
|
message += this.conversion.message;
|
|
}
|
|
predictions = this.conversion.predictions;
|
|
}
|
|
|
|
// Hint if the param is required, but not provided
|
|
var argProvided = this.arg && this.arg.text !== '';
|
|
var dataProvided = this.value !== undefined || argProvided;
|
|
if (this.param.defaultValue === undefined && !dataProvided) {
|
|
status = Status.INVALID;
|
|
message += '<strong>Required<\strong>';
|
|
}
|
|
|
|
return new Hint(status, message, start, end, predictions);
|
|
},
|
|
|
|
/**
|
|
* Basically <tt>setValue(conversion.predictions[0])</tt> done in a safe
|
|
* way.
|
|
*/
|
|
complete: function() {
|
|
if (this.conversion && this.conversion.predictions &&
|
|
this.conversion.predictions.length > 0) {
|
|
this.setValue(this.conversion.predictions[0]);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* If the cursor is at 'position', do we have sufficient data to start
|
|
* displaying the next hint. This is both complex and important.
|
|
* For example, if the user has just typed:<ul>
|
|
* <li>'set tabstop ' then they clearly want to know about the valid
|
|
* values for the tabstop setting, so the hint is based on the next
|
|
* parameter.
|
|
* <li>'set tabstop' (without trailing space) - they will probably still
|
|
* want to know about the valid values for the tabstop setting because
|
|
* there is no confusion about the setting in question.
|
|
* <li>'set tabsto' they've not finished typing a setting name so the hint
|
|
* should be based on the current parameter.
|
|
* <li>'set tabstop' (when there is an additional tabstopstyle setting) we
|
|
* can't make assumptions about the setting - we're not finished.
|
|
* </ul>
|
|
* <p>Note that the input for 2 and 4 is identical, only the configuration
|
|
* has changed, so hint display is environmental.
|
|
*
|
|
* <p>This function works out if the cursor is before the end of this
|
|
* assignment (assuming that we've asked the same thing of the previous
|
|
* assignment) and then attempts to work out if we should use the hint from
|
|
* the next assignment even though technically the cursor is still inside
|
|
* this one due to the rules above.
|
|
*/
|
|
isPositionCaptured: function(position) {
|
|
if (!this.arg) {
|
|
return false;
|
|
}
|
|
|
|
// Note we don't check if position >= this.arg.start because that's
|
|
// implied by the fact that we're asking the assignments in turn, and
|
|
// we want to avoid thing falling between the cracks, but we do need
|
|
// to check that the argument does have a position
|
|
if (this.arg.start === -1) {
|
|
return false;
|
|
}
|
|
|
|
// We're clearly done if the position is past the end of the text
|
|
if (position > this.arg.end) {
|
|
return false;
|
|
}
|
|
|
|
// If we're AT the end, the position is captured if either the status
|
|
// is not valid or if there are other valid options including current
|
|
if (position === this.arg.end) {
|
|
return this.conversion.status !== Status.VALID ||
|
|
this.conversion.predictions.length !== 0;
|
|
}
|
|
|
|
// Otherwise we're clearly inside
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Replace the current value with the lower value if such a concept
|
|
* exists.
|
|
*/
|
|
decrement: function() {
|
|
var replacement = this.param.type.decrement(this.value);
|
|
if (replacement != null) {
|
|
this.setValue(replacement);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Replace the current value with the higher value if such a concept
|
|
* exists.
|
|
*/
|
|
increment: function() {
|
|
var replacement = this.param.type.increment(this.value);
|
|
if (replacement != null) {
|
|
this.setValue(replacement);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Helper when we're rebuilding command lines.
|
|
*/
|
|
toString: function() {
|
|
return this.arg ? this.arg.toString() : '';
|
|
}
|
|
};
|
|
exports.Assignment = Assignment;
|
|
|
|
|
|
/**
|
|
* This is a special parameter to reflect the command itself.
|
|
*/
|
|
var commandParam = {
|
|
name: '__command',
|
|
type: 'command',
|
|
description: 'The command to execute',
|
|
|
|
/**
|
|
* Provide some documentation for a command.
|
|
*/
|
|
getCustomHint: function(command, arg) {
|
|
var docs = [];
|
|
docs.push('<strong><tt> > ');
|
|
docs.push(command.name);
|
|
if (command.params && command.params.length > 0) {
|
|
command.params.forEach(function(param) {
|
|
if (param.defaultValue === undefined) {
|
|
docs.push(' [' + param.name + ']');
|
|
}
|
|
else {
|
|
docs.push(' <em>[' + param.name + ']</em>');
|
|
}
|
|
}, this);
|
|
}
|
|
docs.push('</tt></strong><br/>');
|
|
|
|
docs.push(command.description ? command.description : '(No description)');
|
|
docs.push('<br/>');
|
|
|
|
if (command.params && command.params.length > 0) {
|
|
docs.push('<ul>');
|
|
command.params.forEach(function(param) {
|
|
docs.push('<li>');
|
|
docs.push('<strong><tt>' + param.name + '</tt></strong>: ');
|
|
docs.push(param.description ? param.description : '(No description)');
|
|
if (param.defaultValue === undefined) {
|
|
docs.push(' <em>[Required]</em>');
|
|
}
|
|
else if (param.defaultValue === null) {
|
|
docs.push(' <em>[Optional]</em>');
|
|
}
|
|
else {
|
|
docs.push(' <em>[Default: ' + param.defaultValue + ']</em>');
|
|
}
|
|
docs.push('</li>');
|
|
}, this);
|
|
docs.push('</ul>');
|
|
}
|
|
|
|
return new Hint(Status.VALID, docs.join(''), arg);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A Requisition collects the information needed to execute a command.
|
|
* There is no point in a requisition for parameter-less commands because there
|
|
* is no information to collect. A Requisition is a collection of assignments
|
|
* of values to parameters, each handled by an instance of Assignment.
|
|
* CliRequisition adds functions for parsing input from a command line to this
|
|
* class.
|
|
* <h2>Events<h2>
|
|
* We publish the following events:<ul>
|
|
* <li>argumentChange: The text of some argument has changed. It is likely that
|
|
* any UI component displaying this argument will need to be updated. (Note that
|
|
* this event is actually published by the Argument itself - see the docs for
|
|
* Argument for more details)
|
|
* The event object looks like: { argument: A, oldText: B, text: B }
|
|
* <li>commandChange: The command has changed. It is likely that a UI
|
|
* structure will need updating to match the parameters of the new command.
|
|
* The event object looks like { command: A }
|
|
* @constructor
|
|
*/
|
|
function Requisition(env) {
|
|
this.env = env;
|
|
this.commandAssignment = new Assignment(commandParam, this);
|
|
}
|
|
|
|
Requisition.prototype = {
|
|
/**
|
|
* The command that we are about to execute.
|
|
* @see setCommandConversion()
|
|
* @readonly
|
|
*/
|
|
commandAssignment: undefined,
|
|
|
|
/**
|
|
* The count of assignments. Excludes the commandAssignment
|
|
* @readonly
|
|
*/
|
|
assignmentCount: undefined,
|
|
|
|
/**
|
|
* The object that stores of Assignment objects that we are filling out.
|
|
* The Assignment objects are stored under their param.name for named
|
|
* lookup. Note: We make use of the property of Javascript objects that
|
|
* they are not just hashmaps, but linked-list hashmaps which iterate in
|
|
* insertion order.
|
|
* Excludes the commandAssignment.
|
|
*/
|
|
_assignments: undefined,
|
|
|
|
/**
|
|
* The store of hints generated by the assignments. We are trying to prevent
|
|
* the UI from needing to access this in broad form, but instead use
|
|
* methods that query part of this structure.
|
|
*/
|
|
_hints: undefined,
|
|
|
|
/**
|
|
* When the command changes, we need to keep a bunch of stuff in sync
|
|
*/
|
|
_assignmentChanged: function(assignment) {
|
|
// This is all about re-creating Assignments
|
|
if (assignment.param.name !== '__command') {
|
|
return;
|
|
}
|
|
|
|
this._assignments = {};
|
|
|
|
if (assignment.value) {
|
|
assignment.value.params.forEach(function(param) {
|
|
this._assignments[param.name] = new Assignment(param, this);
|
|
}, this);
|
|
}
|
|
|
|
this.assignmentCount = Object.keys(this._assignments).length;
|
|
this._dispatchEvent('commandChange', { command: assignment.value });
|
|
},
|
|
|
|
/**
|
|
* Assignments have an order, so we need to store them in an array.
|
|
* But we also need named access ...
|
|
*/
|
|
getAssignment: function(nameOrNumber) {
|
|
var name = (typeof nameOrNumber === 'string') ?
|
|
nameOrNumber :
|
|
Object.keys(this._assignments)[nameOrNumber];
|
|
return this._assignments[name];
|
|
},
|
|
|
|
/**
|
|
* Where parameter name == assignment names - they are the same.
|
|
*/
|
|
getParameterNames: function() {
|
|
return Object.keys(this._assignments);
|
|
},
|
|
|
|
/**
|
|
* A *shallow* clone of the assignments.
|
|
* This is useful for systems that wish to go over all the assignments
|
|
* finding values one way or another and wish to trim an array as they go.
|
|
*/
|
|
cloneAssignments: function() {
|
|
return Object.keys(this._assignments).map(function(name) {
|
|
return this._assignments[name];
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* Collect the statuses from the Assignments.
|
|
* The hints returned are sorted by severity
|
|
*/
|
|
_updateHints: function() {
|
|
// TODO: work out when to clear this out for the plain Requisition case
|
|
// this._hints = [];
|
|
this.getAssignments(true).forEach(function(assignment) {
|
|
this._hints.push(assignment.getHint());
|
|
}, this);
|
|
Hint.sort(this._hints);
|
|
|
|
// We would like to put some initial help here, but for anyone but
|
|
// a complete novice a 'type help' message is very annoying, so we
|
|
// need to find a way to only display this message once, or for
|
|
// until the user click a 'close' button or similar
|
|
// TODO: Add special case for '' input
|
|
},
|
|
|
|
/**
|
|
* Returns the most severe status
|
|
*/
|
|
getWorstHint: function() {
|
|
return this._hints[0];
|
|
},
|
|
|
|
/**
|
|
* Extract the names and values of all the assignments, and return as
|
|
* an object.
|
|
*/
|
|
getArgsObject: function() {
|
|
var args = {};
|
|
this.getAssignments().forEach(function(assignment) {
|
|
args[assignment.param.name] = assignment.value;
|
|
}, this);
|
|
return args;
|
|
},
|
|
|
|
/**
|
|
* Access the arguments as an array.
|
|
* @param includeCommand By default only the parameter arguments are
|
|
* returned unless (includeCommand === true), in which case the list is
|
|
* prepended with commandAssignment.arg
|
|
*/
|
|
getAssignments: function(includeCommand) {
|
|
var args = [];
|
|
if (includeCommand === true) {
|
|
args.push(this.commandAssignment);
|
|
}
|
|
Object.keys(this._assignments).forEach(function(name) {
|
|
args.push(this.getAssignment(name));
|
|
}, this);
|
|
return args;
|
|
},
|
|
|
|
/**
|
|
* Reset all the assignments to their default values
|
|
*/
|
|
setDefaultValues: function() {
|
|
this.getAssignments().forEach(function(assignment) {
|
|
assignment.setValue(undefined);
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* Helper to call canon.exec
|
|
*/
|
|
exec: function() {
|
|
canon.exec(this.commandAssignment.value,
|
|
this.env,
|
|
"cli",
|
|
this.getArgsObject(),
|
|
this.toCanonicalString());
|
|
},
|
|
|
|
/**
|
|
* Extract a canonical version of the input
|
|
*/
|
|
toCanonicalString: function() {
|
|
var line = [];
|
|
line.push(this.commandAssignment.value.name);
|
|
Object.keys(this._assignments).forEach(function(name) {
|
|
var assignment = this._assignments[name];
|
|
var type = assignment.param.type;
|
|
// TODO: This will cause problems if there is a non-default value
|
|
// after a default value. Also we need to decide when to use
|
|
// named parameters in place of positional params. Both can wait.
|
|
if (assignment.value !== assignment.param.defaultValue) {
|
|
line.push(' ');
|
|
line.push(type.stringify(assignment.value));
|
|
}
|
|
}, this);
|
|
return line.join('');
|
|
}
|
|
};
|
|
oop.implement(Requisition.prototype, EventEmitter);
|
|
exports.Requisition = Requisition;
|
|
|
|
|
|
/**
|
|
* An object used during command line parsing to hold the various intermediate
|
|
* data steps.
|
|
* <p>The 'output' of the update is held in 2 objects: input.hints which is an
|
|
* array of hints to display to the user. In the future this will become a
|
|
* single value.
|
|
* <p>The other output value is input.requisition which gives access to an
|
|
* args object for use in executing the final command.
|
|
*
|
|
* <p>The majority of the functions in this class are called in sequence by the
|
|
* constructor. Their task is to add to <tt>hints</tt> fill out the requisition.
|
|
* <p>The general sequence is:<ul>
|
|
* <li>_tokenize(): convert _typed into _parts
|
|
* <li>_split(): convert _parts into _command and _unparsedArgs
|
|
* <li>_assign(): convert _unparsedArgs into requisition
|
|
* </ul>
|
|
*
|
|
* @param typed {string} The instruction as typed by the user so far
|
|
* @param options {object} A list of optional named parameters. Can be any of:
|
|
* <b>flags</b>: Flags for us to check against the predicates specified with the
|
|
* commands. Defaulted to <tt>keyboard.buildFlags({ });</tt>
|
|
* if not specified.
|
|
* @constructor
|
|
*/
|
|
function CliRequisition(env, options) {
|
|
Requisition.call(this, env);
|
|
|
|
if (options && options.flags) {
|
|
/**
|
|
* TODO: We were using a default of keyboard.buildFlags({ });
|
|
* This allowed us to have commands that only existed in certain contexts
|
|
* - i.e. Javascript specific commands.
|
|
*/
|
|
this.flags = options.flags;
|
|
}
|
|
}
|
|
oop.inherits(CliRequisition, Requisition);
|
|
(function() {
|
|
/**
|
|
* Called by the UI when ever the user interacts with a command line input
|
|
* @param input A structure that details the state of the input field.
|
|
* It should look something like: { typed:a, cursor: { start:b, end:c } }
|
|
* Where a is the contents of the input field, and b and c are the start
|
|
* and end of the cursor/selection respectively.
|
|
*/
|
|
CliRequisition.prototype.update = function(input) {
|
|
this.input = input;
|
|
this._hints = [];
|
|
|
|
var args = this._tokenize(input.typed);
|
|
this._split(args);
|
|
|
|
if (this.commandAssignment.value) {
|
|
this._assign(args);
|
|
}
|
|
|
|
this._updateHints();
|
|
};
|
|
|
|
/**
|
|
* Return an array of Status scores so we can create a marked up
|
|
* version of the command line input.
|
|
*/
|
|
CliRequisition.prototype.getInputStatusMarkup = function() {
|
|
// 'scores' is an array which tells us what chars are errors
|
|
// Initialize with everything VALID
|
|
var scores = this.toString().split('').map(function(ch) {
|
|
return Status.VALID;
|
|
});
|
|
// For all chars in all hints, check and upgrade the score
|
|
this._hints.forEach(function(hint) {
|
|
for (var i = hint.start; i <= hint.end; i++) {
|
|
if (hint.status > scores[i]) {
|
|
scores[i] = hint.status;
|
|
}
|
|
}
|
|
}, this);
|
|
return scores;
|
|
};
|
|
|
|
/**
|
|
* Reconstitute the input from the args
|
|
*/
|
|
CliRequisition.prototype.toString = function() {
|
|
return this.getAssignments(true).map(function(assignment) {
|
|
return assignment.toString();
|
|
}, this).join('');
|
|
};
|
|
|
|
var superUpdateHints = CliRequisition.prototype._updateHints;
|
|
/**
|
|
* Marks up hints in a number of ways:
|
|
* - Makes INCOMPLETE hints that are not near the cursor INVALID since
|
|
* they can't be completed by typing
|
|
* - Finds the most severe hint, and annotates the array with it
|
|
* - Finds the hint to display, and also annotates the array with it
|
|
* TODO: I'm wondering if array annotation is evil and we should replace
|
|
* this with an object. Need to find out more.
|
|
*/
|
|
CliRequisition.prototype._updateHints = function() {
|
|
superUpdateHints.call(this);
|
|
|
|
// Not knowing about cursor positioning, the requisition and assignments
|
|
// can't know this, but anything they mark as INCOMPLETE is actually
|
|
// INVALID unless the cursor is actually inside that argument.
|
|
var c = this.input.cursor;
|
|
this._hints.forEach(function(hint) {
|
|
var startInHint = c.start >= hint.start && c.start <= hint.end;
|
|
var endInHint = c.end >= hint.start && c.end <= hint.end;
|
|
var inHint = startInHint || endInHint;
|
|
if (!inHint && hint.status === Status.INCOMPLETE) {
|
|
hint.status = Status.INVALID;
|
|
}
|
|
}, this);
|
|
|
|
Hint.sort(this._hints);
|
|
};
|
|
|
|
/**
|
|
* Accessor for the hints array.
|
|
* While we could just use the hints property, using getHints() is
|
|
* preferred for symmetry with Requisition where it needs a function due to
|
|
* lack of an atomic update system.
|
|
*/
|
|
CliRequisition.prototype.getHints = function() {
|
|
return this._hints;
|
|
};
|
|
|
|
/**
|
|
* Look through the arguments attached to our assignments for the assignment
|
|
* at the given position.
|
|
*/
|
|
CliRequisition.prototype.getAssignmentAt = function(position) {
|
|
var assignments = this.getAssignments(true);
|
|
for (var i = 0; i < assignments.length; i++) {
|
|
var assignment = assignments[i];
|
|
if (!assignment.arg) {
|
|
// There is no argument in this assignment, we've fallen off
|
|
// the end of the obvious answers - it must be this one.
|
|
return assignment;
|
|
}
|
|
if (assignment.isPositionCaptured(position)) {
|
|
return assignment;
|
|
}
|
|
}
|
|
|
|
return assignment;
|
|
};
|
|
|
|
/**
|
|
* Split up the input taking into account ' and "
|
|
*/
|
|
CliRequisition.prototype._tokenize = function(typed) {
|
|
// For blank input, place a dummy empty argument into the list
|
|
if (typed == null || typed.length === 0) {
|
|
return [ new Argument(this, '', 0, 0, '', '') ];
|
|
}
|
|
|
|
var OUTSIDE = 1; // The last character was whitespace
|
|
var IN_SIMPLE = 2; // The last character was part of a parameter
|
|
var IN_SINGLE_Q = 3; // We're inside a single quote: '
|
|
var IN_DOUBLE_Q = 4; // We're inside double quotes: "
|
|
|
|
var mode = OUTSIDE;
|
|
|
|
// First we un-escape. This list was taken from:
|
|
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#Unicode
|
|
// We are generally converting to their real values except for \', \"
|
|
// and '\ ' which we are converting to unicode private characters so we
|
|
// can distinguish them from ', " and ' ', which have special meaning.
|
|
// They need swapping back post-split - see unescape2()
|
|
typed = typed
|
|
.replace(/\\\\/g, '\\')
|
|
.replace(/\\b/g, '\b')
|
|
.replace(/\\f/g, '\f')
|
|
.replace(/\\n/g, '\n')
|
|
.replace(/\\r/g, '\r')
|
|
.replace(/\\t/g, '\t')
|
|
.replace(/\\v/g, '\v')
|
|
.replace(/\\n/g, '\n')
|
|
.replace(/\\r/g, '\r')
|
|
.replace(/\\ /g, '\uF000')
|
|
.replace(/\\'/g, '\uF001')
|
|
.replace(/\\"/g, '\uF002');
|
|
|
|
function unescape2(str) {
|
|
return str
|
|
.replace(/\uF000/g, ' ')
|
|
.replace(/\uF001/g, '\'')
|
|
.replace(/\uF002/g, '"');
|
|
}
|
|
|
|
var i = 0;
|
|
var start = 0; // Where did this section start?
|
|
var prefix = '';
|
|
var args = [];
|
|
|
|
while (true) {
|
|
if (i >= typed.length) {
|
|
// There is nothing else to read - tidy up
|
|
if (mode !== OUTSIDE) {
|
|
var str = unescape2(typed.substring(start, i));
|
|
args.push(new Argument(this, str, start, i, prefix, ''));
|
|
}
|
|
else {
|
|
if (i !== start) {
|
|
// There's a bunch of whitespace at the end of the
|
|
// command add it to the last argument's suffix,
|
|
// creating an empty argument if needed.
|
|
var extra = typed.substring(start, i);
|
|
var lastArg = args[args.length - 1];
|
|
if (!lastArg) {
|
|
lastArg = new Argument(this, '', i, i, extra, '');
|
|
args.push(lastArg);
|
|
}
|
|
else {
|
|
lastArg.suffix += extra;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
var c = typed[i];
|
|
switch (mode) {
|
|
case OUTSIDE:
|
|
if (c === '\'') {
|
|
prefix = typed.substring(start, i + 1);
|
|
mode = IN_SINGLE_Q;
|
|
start = i + 1;
|
|
}
|
|
else if (c === '"') {
|
|
prefix = typed.substring(start, i + 1);
|
|
mode = IN_DOUBLE_Q;
|
|
start = i + 1;
|
|
}
|
|
else if (/ /.test(c)) {
|
|
// Still whitespace, do nothing
|
|
}
|
|
else {
|
|
prefix = typed.substring(start, i);
|
|
mode = IN_SIMPLE;
|
|
start = i;
|
|
}
|
|
break;
|
|
|
|
case IN_SIMPLE:
|
|
// There is an edge case of xx'xx which we are assuming to
|
|
// be a single parameter (and same with ")
|
|
if (c === ' ') {
|
|
var str = unescape2(typed.substring(start, i));
|
|
args.push(new Argument(this, str,
|
|
start, i, prefix, ''));
|
|
mode = OUTSIDE;
|
|
start = i;
|
|
prefix = '';
|
|
}
|
|
break;
|
|
|
|
case IN_SINGLE_Q:
|
|
if (c === '\'') {
|
|
var str = unescape2(typed.substring(start, i));
|
|
args.push(new Argument(this, str,
|
|
start - 1, i + 1, prefix, c));
|
|
mode = OUTSIDE;
|
|
start = i + 1;
|
|
prefix = '';
|
|
}
|
|
break;
|
|
|
|
case IN_DOUBLE_Q:
|
|
if (c === '"') {
|
|
var str = unescape2(typed.substring(start, i));
|
|
args.push(new Argument(this, str,
|
|
start - 1, i + 1, prefix, c));
|
|
mode = OUTSIDE;
|
|
start = i + 1;
|
|
prefix = '';
|
|
}
|
|
break;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
return args;
|
|
};
|
|
|
|
/**
|
|
* Looks in the canon for a command extension that matches what has been
|
|
* typed at the command line.
|
|
*/
|
|
CliRequisition.prototype._split = function(args) {
|
|
var argsUsed = 1;
|
|
var arg;
|
|
|
|
while (argsUsed <= args.length) {
|
|
var arg = Argument.merge(args, 0, argsUsed);
|
|
this.commandAssignment.setArgument(arg);
|
|
|
|
if (!this.commandAssignment.value) {
|
|
// Not found. break with value == null
|
|
break;
|
|
}
|
|
|
|
/*
|
|
// Previously we needed a way to hide commands depending context.
|
|
// We have not resurrected that feature yet.
|
|
if (!keyboard.flagsMatch(command.predicates, this.flags)) {
|
|
// If the predicates say 'no match' then go LA LA LA
|
|
command = null;
|
|
break;
|
|
}
|
|
*/
|
|
|
|
if (this.commandAssignment.value.exec) {
|
|
// Valid command, break with command valid
|
|
for (var i = 0; i < argsUsed; i++) {
|
|
args.shift();
|
|
}
|
|
break;
|
|
}
|
|
|
|
argsUsed++;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Work out which arguments are applicable to which parameters.
|
|
* <p>This takes #_command.params and #_unparsedArgs and creates a map of
|
|
* param names to 'assignment' objects, which have the following properties:
|
|
* <ul>
|
|
* <li>param - The matching parameter.
|
|
* <li>index - Zero based index into where the match came from on the input
|
|
* <li>value - The matching input
|
|
* </ul>
|
|
*/
|
|
CliRequisition.prototype._assign = function(args) {
|
|
if (args.length === 0) {
|
|
this.setDefaultValues();
|
|
return;
|
|
}
|
|
|
|
// Create an error if the command does not take parameters, but we have
|
|
// been given them ...
|
|
if (this.assignmentCount === 0) {
|
|
// TODO: previously we were doing some extra work to avoid this if
|
|
// we determined that we had args that were all whitespace, but
|
|
// probably given our tighter tokenize() this won't be an issue?
|
|
this._hints.push(new Hint(Status.INVALID,
|
|
this.commandAssignment.value.name +
|
|
' does not take any parameters',
|
|
Argument.merge(args)));
|
|
return;
|
|
}
|
|
|
|
// Special case: if there is only 1 parameter, and that's of type
|
|
// text we put all the params into the first param
|
|
if (this.assignmentCount === 1) {
|
|
var assignment = this.getAssignment(0);
|
|
if (assignment.param.type.name === 'text') {
|
|
assignment.setArgument(Argument.merge(args));
|
|
return;
|
|
}
|
|
}
|
|
|
|
var assignments = this.cloneAssignments();
|
|
var names = this.getParameterNames();
|
|
|
|
// Extract all the named parameters
|
|
var used = [];
|
|
assignments.forEach(function(assignment) {
|
|
var namedArgText = '--' + assignment.name;
|
|
|
|
var i = 0;
|
|
while (true) {
|
|
var arg = args[i];
|
|
if (namedArgText !== arg.text) {
|
|
i++;
|
|
if (i >= args.length) {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// boolean parameters don't have values, default to false
|
|
if (assignment.param.type.name === 'boolean') {
|
|
assignment.setValue(true);
|
|
}
|
|
else {
|
|
if (i + 1 < args.length) {
|
|
// Missing value portion of this named param
|
|
this._hints.push(new Hint(Status.INCOMPLETE,
|
|
'Missing value for: ' + namedArgText,
|
|
args[i]));
|
|
}
|
|
else {
|
|
args.splice(i + 1, 1);
|
|
assignment.setArgument(args[i + 1]);
|
|
}
|
|
}
|
|
|
|
lang.arrayRemove(names, assignment.name);
|
|
args.splice(i, 1);
|
|
// We don't need to i++ if we splice
|
|
}
|
|
}, this);
|
|
|
|
// What's left are positional parameters assign in order
|
|
names.forEach(function(name) {
|
|
var assignment = this.getAssignment(name);
|
|
if (args.length === 0) {
|
|
// No more values
|
|
assignment.setValue(undefined); // i.e. default
|
|
}
|
|
else {
|
|
var arg = args[0];
|
|
args.splice(0, 1);
|
|
assignment.setArgument(arg);
|
|
}
|
|
}, this);
|
|
|
|
if (args.length > 0) {
|
|
var remaining = Argument.merge(args);
|
|
this._hints.push(new Hint(Status.INVALID,
|
|
'Input \'' + remaining.text + '\' makes no sense.',
|
|
remaining));
|
|
}
|
|
};
|
|
|
|
})();
|
|
exports.CliRequisition = CliRequisition;
|
|
|
|
|
|
});
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Mozilla Skywriter.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2009
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Joe Walker (jwalker@mozilla.com)
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
define('cockpit/ui/settings', ['require', 'exports', 'module' , 'pilot/types', 'pilot/types/basic'], function(require, exports, module) {
|
|
|
|
|
|
var types = require("pilot/types");
|
|
var SelectionType = require('pilot/types/basic').SelectionType;
|
|
|
|
var direction = new SelectionType({
|
|
name: 'direction',
|
|
data: [ 'above', 'below' ]
|
|
});
|
|
|
|
var hintDirectionSetting = {
|
|
name: "hintDirection",
|
|
description: "Are hints shown above or below the command line?",
|
|
type: "direction",
|
|
defaultValue: "above"
|
|
};
|
|
|
|
var outputDirectionSetting = {
|
|
name: "outputDirection",
|
|
description: "Is the output window shown above or below the command line?",
|
|
type: "direction",
|
|
defaultValue: "above"
|
|
};
|
|
|
|
var outputHeightSetting = {
|
|
name: "outputHeight",
|
|
description: "What height should the output panel be?",
|
|
type: "number",
|
|
defaultValue: 300
|
|
};
|
|
|
|
exports.startup = function(data, reason) {
|
|
types.registerType(direction);
|
|
data.env.settings.addSetting(hintDirectionSetting);
|
|
data.env.settings.addSetting(outputDirectionSetting);
|
|
data.env.settings.addSetting(outputHeightSetting);
|
|
};
|
|
|
|
exports.shutdown = function(data, reason) {
|
|
types.unregisterType(direction);
|
|
data.env.settings.removeSetting(hintDirectionSetting);
|
|
data.env.settings.removeSetting(outputDirectionSetting);
|
|
data.env.settings.removeSetting(outputHeightSetting);
|
|
};
|
|
|
|
|
|
});
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Skywriter.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2009
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Joe Walker (jwalker@mozilla.com)
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
define('cockpit/ui/cli_view', ['require', 'exports', 'module' , 'text!cockpit/ui/cli_view.css', 'pilot/event', 'pilot/dom', 'pilot/keys', 'pilot/canon', 'pilot/types', 'cockpit/cli', 'cockpit/ui/request_view'], function(require, exports, module) {
|
|
|
|
|
|
var editorCss = require("text!cockpit/ui/cli_view.css");
|
|
var event = require("pilot/event");
|
|
var dom = require("pilot/dom");
|
|
|
|
dom.importCssString(editorCss);
|
|
|
|
var event = require("pilot/event");
|
|
var keys = require("pilot/keys");
|
|
var canon = require("pilot/canon");
|
|
var Status = require('pilot/types').Status;
|
|
|
|
var CliRequisition = require('cockpit/cli').CliRequisition;
|
|
var Hint = require('cockpit/cli').Hint;
|
|
var RequestView = require('cockpit/ui/request_view').RequestView;
|
|
|
|
var NO_HINT = new Hint(Status.VALID, '', 0, 0);
|
|
|
|
/**
|
|
* On startup we need to:
|
|
* 1. Add 3 sets of elements to the DOM for:
|
|
* - command line output
|
|
* - input hints
|
|
* - completion
|
|
* 2. Attach a set of events so the command line works
|
|
*/
|
|
exports.startup = function(data, reason) {
|
|
var cli = new CliRequisition(data.env);
|
|
var cliView = new CliView(cli, data.env);
|
|
data.env.cli = cli;
|
|
};
|
|
|
|
/**
|
|
* A class to handle the simplest UI implementation
|
|
*/
|
|
function CliView(cli, env) {
|
|
cli.cliView = this;
|
|
this.cli = cli;
|
|
this.doc = document;
|
|
this.win = dom.getParentWindow(this.doc);
|
|
this.env = env;
|
|
|
|
// TODO: we should have a better way to specify command lines???
|
|
this.element = this.doc.getElementById('cockpitInput');
|
|
if (!this.element) {
|
|
// console.log('No element with an id of cockpit. Bailing on cli');
|
|
return;
|
|
}
|
|
|
|
this.settings = env.settings;
|
|
this.hintDirection = this.settings.getSetting('hintDirection');
|
|
this.outputDirection = this.settings.getSetting('outputDirection');
|
|
this.outputHeight = this.settings.getSetting('outputHeight');
|
|
|
|
// If the requisition tells us something has changed, we use this to know
|
|
// if we should ignore it
|
|
this.isUpdating = false;
|
|
|
|
this.createElements();
|
|
this.update();
|
|
}
|
|
CliView.prototype = {
|
|
/**
|
|
* Create divs for completion, hints and output
|
|
*/
|
|
createElements: function() {
|
|
var input = this.element;
|
|
|
|
this.element.spellcheck = false;
|
|
|
|
this.output = this.doc.getElementById('cockpitOutput');
|
|
this.popupOutput = (this.output == null);
|
|
if (!this.output) {
|
|
this.output = this.doc.createElement('div');
|
|
this.output.id = 'cockpitOutput';
|
|
this.output.className = 'cptOutput';
|
|
input.parentNode.insertBefore(this.output, input.nextSibling);
|
|
|
|
var setMaxOutputHeight = function() {
|
|
this.output.style.maxHeight = this.outputHeight.get() + 'px';
|
|
}.bind(this);
|
|
this.outputHeight.addEventListener('change', setMaxOutputHeight);
|
|
setMaxOutputHeight();
|
|
}
|
|
|
|
this.completer = this.doc.createElement('div');
|
|
this.completer.className = 'cptCompletion VALID';
|
|
|
|
this.completer.style.color = dom.computedStyle(input, "color");
|
|
this.completer.style.fontSize = dom.computedStyle(input, "fontSize");
|
|
this.completer.style.fontFamily = dom.computedStyle(input, "fontFamily");
|
|
this.completer.style.fontWeight = dom.computedStyle(input, "fontWeight");
|
|
this.completer.style.fontStyle = dom.computedStyle(input, "fontStyle");
|
|
input.parentNode.insertBefore(this.completer, input.nextSibling);
|
|
|
|
// Transfer background styling to the completer.
|
|
this.completer.style.backgroundColor = input.style.backgroundColor;
|
|
input.style.backgroundColor = 'transparent';
|
|
|
|
this.hinter = this.doc.createElement('div');
|
|
this.hinter.className = 'cptHints';
|
|
input.parentNode.insertBefore(this.hinter, input.nextSibling);
|
|
|
|
var resizer = this.resizer.bind(this);
|
|
event.addListener(this.win, 'resize', resizer);
|
|
this.hintDirection.addEventListener('change', resizer);
|
|
this.outputDirection.addEventListener('change', resizer);
|
|
resizer();
|
|
|
|
canon.addEventListener('output', function(ev) {
|
|
new RequestView(ev.request, this);
|
|
}.bind(this));
|
|
event.addCommandKeyListener(input, this.onCommandKey.bind(this));
|
|
event.addListener(input, 'keyup', this.onKeyUp.bind(this));
|
|
|
|
// cursor position affects hint severity. TODO: shortcuts for speed
|
|
event.addListener(input, 'mouseup', function(ev) {
|
|
this.isUpdating = true;
|
|
this.update();
|
|
this.isUpdating = false;
|
|
}.bind(this));
|
|
|
|
this.cli.addEventListener('argumentChange', this.onArgChange.bind(this));
|
|
|
|
event.addListener(input, "focus", function() {
|
|
dom.addCssClass(this.output, "cptFocusPopup");
|
|
dom.addCssClass(this.hinter, "cptFocusPopup");
|
|
}.bind(this));
|
|
|
|
function hideOutput() {
|
|
dom.removeCssClass(this.output, "cptFocusPopup");
|
|
dom.removeCssClass(this.hinter, "cptFocusPopup");
|
|
};
|
|
event.addListener(input, "blur", hideOutput.bind(this));
|
|
hideOutput.call(this);
|
|
},
|
|
|
|
/**
|
|
* We need to see the output of the latest command entered
|
|
*/
|
|
scrollOutputToBottom: function() {
|
|
// Certain browsers have a bug such that scrollHeight is too small
|
|
// when content does not fill the client area of the element
|
|
var scrollHeight = Math.max(this.output.scrollHeight, this.output.clientHeight);
|
|
this.output.scrollTop = scrollHeight - this.output.clientHeight;
|
|
},
|
|
|
|
/**
|
|
* To be called on window resize or any time we want to align the elements
|
|
* with the input box.
|
|
*/
|
|
resizer: function() {
|
|
var rect = this.element.getClientRects()[0];
|
|
|
|
this.completer.style.top = rect.top + 'px';
|
|
var height = rect.bottom - rect.top;
|
|
this.completer.style.height = height + 'px';
|
|
this.completer.style.lineHeight = height + 'px';
|
|
this.completer.style.left = rect.left + 'px';
|
|
var width = rect.right - rect.left;
|
|
this.completer.style.width = width + 'px';
|
|
|
|
if (this.hintDirection.get() === 'below') {
|
|
this.hinter.style.top = rect.bottom + 'px';
|
|
this.hinter.style.bottom = 'auto';
|
|
}
|
|
else {
|
|
this.hinter.style.top = 'auto';
|
|
this.hinter.style.bottom = (this.doc.documentElement.clientHeight - rect.top) + 'px';
|
|
}
|
|
this.hinter.style.left = (rect.left + 30) + 'px';
|
|
this.hinter.style.maxWidth = (width - 110) + 'px';
|
|
|
|
if (this.popupOutput) {
|
|
if (this.outputDirection.get() === 'below') {
|
|
this.output.style.top = rect.bottom + 'px';
|
|
this.output.style.bottom = 'auto';
|
|
}
|
|
else {
|
|
this.output.style.top = 'auto';
|
|
this.output.style.bottom = (this.doc.documentElement.clientHeight - rect.top) + 'px';
|
|
}
|
|
this.output.style.left = rect.left + 'px';
|
|
this.output.style.width = (width - 80) + 'px';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Ensure that TAB isn't handled by the browser
|
|
*/
|
|
onCommandKey: function(ev, hashId, keyCode) {
|
|
var stopEvent;
|
|
if (keyCode === keys.TAB ||
|
|
keyCode === keys.UP ||
|
|
keyCode === keys.DOWN) {
|
|
stopEvent = true;
|
|
} else if (hashId != 0 || keyCode != 0) {
|
|
stopEvent = canon.execKeyCommand(this.env, 'cli', hashId, keyCode);
|
|
}
|
|
stopEvent && event.stopEvent(ev);
|
|
},
|
|
|
|
/**
|
|
* The main keyboard processing loop
|
|
*/
|
|
onKeyUp: function(ev) {
|
|
var handled;
|
|
/*
|
|
var handled = keyboardManager.processKeyEvent(ev, this, {
|
|
isCommandLine: true, isKeyUp: true
|
|
});
|
|
*/
|
|
|
|
// RETURN does a special exec/highlight thing
|
|
if (ev.keyCode === keys.RETURN) {
|
|
var worst = this.cli.getWorstHint();
|
|
// Deny RETURN unless the command might work
|
|
if (worst.status === Status.VALID) {
|
|
this.cli.exec();
|
|
this.element.value = '';
|
|
}
|
|
else {
|
|
// If we've denied RETURN because the command was not VALID,
|
|
// select the part of the command line that is causing problems
|
|
// TODO: if there are 2 errors are we picking the right one?
|
|
dom.setSelectionStart(this.element, worst.start);
|
|
dom.setSelectionEnd(this.element, worst.end);
|
|
}
|
|
}
|
|
|
|
this.update();
|
|
|
|
// Special actions which delegate to the assignment
|
|
var current = this.cli.getAssignmentAt(dom.getSelectionStart(this.element));
|
|
if (current) {
|
|
// TAB does a special complete thing
|
|
if (ev.keyCode === keys.TAB) {
|
|
current.complete();
|
|
this.update();
|
|
}
|
|
|
|
// UP/DOWN look for some history
|
|
if (ev.keyCode === keys.UP) {
|
|
current.increment();
|
|
this.update();
|
|
}
|
|
if (ev.keyCode === keys.DOWN) {
|
|
current.decrement();
|
|
this.update();
|
|
}
|
|
}
|
|
|
|
return handled;
|
|
},
|
|
|
|
/**
|
|
* Actually parse the input and make sure we're all up to date
|
|
*/
|
|
update: function() {
|
|
this.isUpdating = true;
|
|
var input = {
|
|
typed: this.element.value,
|
|
cursor: {
|
|
start: dom.getSelectionStart(this.element),
|
|
end: dom.getSelectionEnd(this.element.selectionEnd)
|
|
}
|
|
};
|
|
this.cli.update(input);
|
|
|
|
var display = this.cli.getAssignmentAt(input.cursor.start).getHint();
|
|
|
|
// 1. Update the completer with prompt/error marker/TAB info
|
|
dom.removeCssClass(this.completer, Status.VALID.toString());
|
|
dom.removeCssClass(this.completer, Status.INCOMPLETE.toString());
|
|
dom.removeCssClass(this.completer, Status.INVALID.toString());
|
|
|
|
var completion = '<span class="cptPrompt">></span> ';
|
|
if (this.element.value.length > 0) {
|
|
var scores = this.cli.getInputStatusMarkup();
|
|
completion += this.markupStatusScore(scores);
|
|
}
|
|
|
|
// Display the "-> prediction" at the end of the completer
|
|
if (this.element.value.length > 0 &&
|
|
display.predictions && display.predictions.length > 0) {
|
|
var tab = display.predictions[0];
|
|
completion += ' ⇥ ' + (tab.name ? tab.name : tab);
|
|
}
|
|
this.completer.innerHTML = completion;
|
|
dom.addCssClass(this.completer, this.cli.getWorstHint().status.toString());
|
|
|
|
// 2. Update the hint element
|
|
var hint = '';
|
|
if (this.element.value.length !== 0) {
|
|
hint += display.message;
|
|
if (display.predictions && display.predictions.length > 0) {
|
|
hint += ': [ ';
|
|
display.predictions.forEach(function(prediction) {
|
|
hint += (prediction.name ? prediction.name : prediction);
|
|
hint += ' | ';
|
|
}, this);
|
|
hint = hint.replace(/\| $/, ']');
|
|
}
|
|
}
|
|
|
|
this.hinter.innerHTML = hint;
|
|
if (hint.length === 0) {
|
|
dom.addCssClass(this.hinter, 'cptNoPopup');
|
|
}
|
|
else {
|
|
dom.removeCssClass(this.hinter, 'cptNoPopup');
|
|
}
|
|
|
|
this.isUpdating = false;
|
|
},
|
|
|
|
/**
|
|
* Markup an array of Status values with spans
|
|
*/
|
|
markupStatusScore: function(scores) {
|
|
var completion = '';
|
|
// Create mark-up
|
|
var i = 0;
|
|
var lastStatus = -1;
|
|
while (true) {
|
|
if (lastStatus !== scores[i]) {
|
|
completion += '<span class=' + scores[i].toString() + '>';
|
|
lastStatus = scores[i];
|
|
}
|
|
completion += this.element.value[i];
|
|
i++;
|
|
if (i === this.element.value.length) {
|
|
completion += '</span>';
|
|
break;
|
|
}
|
|
if (lastStatus !== scores[i]) {
|
|
completion += '</span>';
|
|
}
|
|
}
|
|
|
|
return completion;
|
|
},
|
|
|
|
/**
|
|
* Update the input element to reflect the changed argument
|
|
*/
|
|
onArgChange: function(ev) {
|
|
if (this.isUpdating) {
|
|
return;
|
|
}
|
|
|
|
var prefix = this.element.value.substring(0, ev.argument.start);
|
|
var suffix = this.element.value.substring(ev.argument.end);
|
|
var insert = typeof ev.text === 'string' ? ev.text : ev.text.name;
|
|
this.element.value = prefix + insert + suffix;
|
|
// Fix the cursor.
|
|
var insertEnd = (prefix + insert).length;
|
|
this.element.selectionStart = insertEnd;
|
|
this.element.selectionEnd = insertEnd;
|
|
}
|
|
};
|
|
exports.CliView = CliView;
|
|
|
|
|
|
});
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Skywriter.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2009
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Joe Walker (jwalker@mozilla.com)
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
define('cockpit/ui/request_view', ['require', 'exports', 'module' , 'pilot/dom', 'pilot/event', 'text!cockpit/ui/request_view.html', 'pilot/domtemplate', 'text!cockpit/ui/request_view.css'], function(require, exports, module) {
|
|
|
|
var dom = require("pilot/dom");
|
|
var event = require("pilot/event");
|
|
var requestViewHtml = require("text!cockpit/ui/request_view.html");
|
|
var Templater = require("pilot/domtemplate").Templater;
|
|
|
|
var requestViewCss = require("text!cockpit/ui/request_view.css");
|
|
dom.importCssString(requestViewCss);
|
|
|
|
/**
|
|
* Pull the HTML into the DOM, but don't add it to the document
|
|
*/
|
|
var templates = document.createElement('div');
|
|
templates.innerHTML = requestViewHtml;
|
|
var row = templates.querySelector('.cptRow');
|
|
|
|
/**
|
|
* Work out the path for images.
|
|
* TODO: This should probably live in some utility area somewhere
|
|
*/
|
|
function imageUrl(path) {
|
|
var dataUrl;
|
|
try {
|
|
dataUrl = require('text!cockpit/ui/' + path);
|
|
} catch (e) { }
|
|
if (dataUrl) {
|
|
return dataUrl;
|
|
}
|
|
|
|
var filename = module.id.split('/').pop() + '.js';
|
|
var imagePath;
|
|
|
|
if (module.uri.substr(-filename.length) !== filename) {
|
|
console.error('Can\'t work out path from module.uri/module.id');
|
|
return path;
|
|
}
|
|
|
|
if (module.uri) {
|
|
var end = module.uri.length - filename.length - 1;
|
|
return module.uri.substr(0, end) + "/" + path;
|
|
}
|
|
|
|
return filename + path;
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds a row to the CLI output display
|
|
*/
|
|
function RequestView(request, cliView) {
|
|
this.request = request;
|
|
this.cliView = cliView;
|
|
this.imageUrl = imageUrl;
|
|
|
|
// Elements attached to this by the templater. For info only
|
|
this.rowin = null;
|
|
this.rowout = null;
|
|
this.output = null;
|
|
this.hide = null;
|
|
this.show = null;
|
|
this.duration = null;
|
|
this.throb = null;
|
|
|
|
new Templater().processNode(row.cloneNode(true), this);
|
|
|
|
this.cliView.output.appendChild(this.rowin);
|
|
this.cliView.output.appendChild(this.rowout);
|
|
|
|
this.request.addEventListener('output', this.onRequestChange.bind(this));
|
|
};
|
|
|
|
RequestView.prototype = {
|
|
/**
|
|
* A single click on an invocation line in the console copies the command to
|
|
* the command line
|
|
*/
|
|
copyToInput: function() {
|
|
this.cliView.element.value = this.request.typed;
|
|
},
|
|
|
|
/**
|
|
* A double click on an invocation line in the console executes the command
|
|
*/
|
|
executeRequest: function(ev) {
|
|
this.cliView.cli.update({
|
|
typed: this.request.typed,
|
|
cursor: { start:0, end:0 }
|
|
});
|
|
this.cliView.cli.exec();
|
|
},
|
|
|
|
hideOutput: function(ev) {
|
|
this.output.style.display = 'none';
|
|
dom.addCssClass(this.hide, 'cmd_hidden');
|
|
dom.removeCssClass(this.show, 'cmd_hidden');
|
|
|
|
event.stopPropagation(ev);
|
|
},
|
|
|
|
showOutput: function(ev) {
|
|
this.output.style.display = 'block';
|
|
dom.removeCssClass(this.hide, 'cmd_hidden');
|
|
dom.addCssClass(this.show, 'cmd_hidden');
|
|
|
|
event.stopPropagation(ev);
|
|
},
|
|
|
|
remove: function(ev) {
|
|
this.cliView.output.removeChild(this.rowin);
|
|
this.cliView.output.removeChild(this.rowout);
|
|
event.stopPropagation(ev);
|
|
},
|
|
|
|
onRequestChange: function(ev) {
|
|
this.duration.innerHTML = this.request.duration ?
|
|
'completed in ' + (this.request.duration / 1000) + ' sec ' :
|
|
'';
|
|
|
|
this.output.innerHTML = '';
|
|
this.request.outputs.forEach(function(output) {
|
|
var node;
|
|
if (typeof output == 'string') {
|
|
node = document.createElement('p');
|
|
node.innerHTML = output;
|
|
} else {
|
|
node = output;
|
|
}
|
|
this.output.appendChild(node);
|
|
}, this);
|
|
this.cliView.scrollOutputToBottom();
|
|
|
|
dom.setCssClass(this.output, 'cmd_error', this.request.error);
|
|
|
|
this.throb.style.display = this.request.completed ? 'none' : 'block';
|
|
}
|
|
};
|
|
exports.RequestView = RequestView;
|
|
|
|
|
|
});
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is DomTemplate.
|
|
*
|
|
* The Initial Developer of the Original Code is Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2009
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Joe Walker (jwalker@mozilla.com) (original author)
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
define('pilot/domtemplate', ['require', 'exports', 'module' ], function(require, exports, module) {
|
|
|
|
|
|
// WARNING: do not 'use_strict' without reading the notes in envEval;
|
|
|
|
/**
|
|
* A templater that allows one to quickly template DOM nodes.
|
|
*/
|
|
function Templater() {
|
|
this.scope = [];
|
|
};
|
|
|
|
/**
|
|
* Recursive function to walk the tree processing the attributes as it goes.
|
|
* @param node the node to process. If you pass a string in instead of a DOM
|
|
* element, it is assumed to be an id for use with document.getElementById()
|
|
* @param data the data to use for node processing.
|
|
*/
|
|
Templater.prototype.processNode = function(node, data) {
|
|
if (typeof node === 'string') {
|
|
node = document.getElementById(node);
|
|
}
|
|
if (data === null || data === undefined) {
|
|
data = {};
|
|
}
|
|
this.scope.push(node.nodeName + (node.id ? '#' + node.id : ''));
|
|
try {
|
|
// Process attributes
|
|
if (node.attributes && node.attributes.length) {
|
|
// We need to handle 'foreach' and 'if' first because they might stop
|
|
// some types of processing from happening, and foreach must come first
|
|
// because it defines new data on which 'if' might depend.
|
|
if (node.hasAttribute('foreach')) {
|
|
this.processForEach(node, data);
|
|
return;
|
|
}
|
|
if (node.hasAttribute('if')) {
|
|
if (!this.processIf(node, data)) {
|
|
return;
|
|
}
|
|
}
|
|
// Only make the node available once we know it's not going away
|
|
data.__element = node;
|
|
// It's good to clean up the attributes when we've processed them,
|
|
// but if we do it straight away, we mess up the array index
|
|
var attrs = Array.prototype.slice.call(node.attributes);
|
|
for (var i = 0; i < attrs.length; i++) {
|
|
var value = attrs[i].value;
|
|
var name = attrs[i].name;
|
|
this.scope.push(name);
|
|
try {
|
|
if (name === 'save') {
|
|
// Save attributes are a setter using the node
|
|
value = this.stripBraces(value);
|
|
this.property(value, data, node);
|
|
node.removeAttribute('save');
|
|
} else if (name.substring(0, 2) === 'on') {
|
|
// Event registration relies on property doing a bind
|
|
value = this.stripBraces(value);
|
|
var func = this.property(value, data);
|
|
if (typeof func !== 'function') {
|
|
this.handleError('Expected ' + value +
|
|
' to resolve to a function, but got ' + typeof func);
|
|
}
|
|
node.removeAttribute(name);
|
|
var capture = node.hasAttribute('capture' + name.substring(2));
|
|
node.addEventListener(name.substring(2), func, capture);
|
|
if (capture) {
|
|
node.removeAttribute('capture' + name.substring(2));
|
|
}
|
|
} else {
|
|
// Replace references in all other attributes
|
|
var self = this;
|
|
var newValue = value.replace(/\$\{[^}]*\}/g, function(path) {
|
|
return self.envEval(path.slice(2, -1), data, value);
|
|
});
|
|
// Remove '_' prefix of attribute names so the DOM won't try
|
|
// to use them before we've processed the template
|
|
if (name.charAt(0) === '_') {
|
|
node.removeAttribute(name);
|
|
node.setAttribute(name.substring(1), newValue);
|
|
} else if (value !== newValue) {
|
|
attrs[i].value = newValue;
|
|
}
|
|
}
|
|
} finally {
|
|
this.scope.pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Loop through our children calling processNode. First clone them, so the
|
|
// set of nodes that we visit will be unaffected by additions or removals.
|
|
var childNodes = Array.prototype.slice.call(node.childNodes);
|
|
for (var j = 0; j < childNodes.length; j++) {
|
|
this.processNode(childNodes[j], data);
|
|
}
|
|
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
this.processTextNode(node, data);
|
|
}
|
|
} finally {
|
|
this.scope.pop();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle <x if="${...}">
|
|
* @param node An element with an 'if' attribute
|
|
* @param data The data to use with envEval
|
|
* @returns true if processing should continue, false otherwise
|
|
*/
|
|
Templater.prototype.processIf = function(node, data) {
|
|
this.scope.push('if');
|
|
try {
|
|
var originalValue = node.getAttribute('if');
|
|
var value = this.stripBraces(originalValue);
|
|
var recurse = true;
|
|
try {
|
|
var reply = this.envEval(value, data, originalValue);
|
|
recurse = !!reply;
|
|
} catch (ex) {
|
|
this.handleError('Error with \'' + value + '\'', ex);
|
|
recurse = false;
|
|
}
|
|
if (!recurse) {
|
|
node.parentNode.removeChild(node);
|
|
}
|
|
node.removeAttribute('if');
|
|
return recurse;
|
|
} finally {
|
|
this.scope.pop();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle <x foreach="param in ${array}"> and the special case of
|
|
* <loop foreach="param in ${array}">
|
|
* @param node An element with a 'foreach' attribute
|
|
* @param data The data to use with envEval
|
|
*/
|
|
Templater.prototype.processForEach = function(node, data) {
|
|
this.scope.push('foreach');
|
|
try {
|
|
var originalValue = node.getAttribute('foreach');
|
|
var value = originalValue;
|
|
|
|
var paramName = 'param';
|
|
if (value.charAt(0) === '$') {
|
|
// No custom loop variable name. Use the default: 'param'
|
|
value = this.stripBraces(value);
|
|
} else {
|
|
// Extract the loop variable name from 'NAME in ${ARRAY}'
|
|
var nameArr = value.split(' in ');
|
|
paramName = nameArr[0].trim();
|
|
value = this.stripBraces(nameArr[1].trim());
|
|
}
|
|
node.removeAttribute('foreach');
|
|
try {
|
|
var self = this;
|
|
// Process a single iteration of a loop
|
|
var processSingle = function(member, clone, ref) {
|
|
ref.parentNode.insertBefore(clone, ref);
|
|
data[paramName] = member;
|
|
self.processNode(clone, data);
|
|
delete data[paramName];
|
|
};
|
|
|
|
// processSingle is no good for <loop> nodes where we want to work on
|
|
// the childNodes rather than the node itself
|
|
var processAll = function(scope, member) {
|
|
self.scope.push(scope);
|
|
try {
|
|
if (node.nodeName === 'LOOP') {
|
|
for (var i = 0; i < node.childNodes.length; i++) {
|
|
var clone = node.childNodes[i].cloneNode(true);
|
|
processSingle(member, clone, node);
|
|
}
|
|
} else {
|
|
var clone = node.cloneNode(true);
|
|
clone.removeAttribute('foreach');
|
|
processSingle(member, clone, node);
|
|
}
|
|
} finally {
|
|
self.scope.pop();
|
|
}
|
|
};
|
|
|
|
var reply = this.envEval(value, data, originalValue);
|
|
if (Array.isArray(reply)) {
|
|
reply.forEach(function(data, i) {
|
|
processAll('' + i, data);
|
|
}, this);
|
|
} else {
|
|
for (var param in reply) {
|
|
if (reply.hasOwnProperty(param)) {
|
|
processAll(param, param);
|
|
}
|
|
}
|
|
}
|
|
node.parentNode.removeChild(node);
|
|
} catch (ex) {
|
|
this.handleError('Error with \'' + value + '\'', ex);
|
|
}
|
|
} finally {
|
|
this.scope.pop();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Take a text node and replace it with another text node with the ${...}
|
|
* sections parsed out. We replace the node by altering node.parentNode but
|
|
* we could probably use a DOM Text API to achieve the same thing.
|
|
* @param node The Text node to work on
|
|
* @param data The data to use in calls to envEval
|
|
*/
|
|
Templater.prototype.processTextNode = function(node, data) {
|
|
// Replace references in other attributes
|
|
var value = node.data;
|
|
// We can't use the string.replace() with function trick (see generic
|
|
// attribute processing in processNode()) because we need to support
|
|
// functions that return DOM nodes, so we can't have the conversion to a
|
|
// string.
|
|
// Instead we process the string as an array of parts. In order to split
|
|
// the string up, we first replace '${' with '\uF001$' and '}' with '\uF002'
|
|
// We can then split using \uF001 or \uF002 to get an array of strings
|
|
// where scripts are prefixed with $.
|
|
// \uF001 and \uF002 are just unicode chars reserved for private use.
|
|
value = value.replace(/\$\{([^}]*)\}/g, '\uF001$$$1\uF002');
|
|
var parts = value.split(/\uF001|\uF002/);
|
|
if (parts.length > 1) {
|
|
parts.forEach(function(part) {
|
|
if (part === null || part === undefined || part === '') {
|
|
return;
|
|
}
|
|
if (part.charAt(0) === '$') {
|
|
part = this.envEval(part.slice(1), data, node.data);
|
|
}
|
|
// It looks like this was done a few lines above but see envEval
|
|
if (part === null) {
|
|
part = "null";
|
|
}
|
|
if (part === undefined) {
|
|
part = "undefined";
|
|
}
|
|
// if (isDOMElement(part)) { ... }
|
|
if (typeof part.cloneNode !== 'function') {
|
|
part = node.ownerDocument.createTextNode(part.toString());
|
|
}
|
|
node.parentNode.insertBefore(part, node);
|
|
}, this);
|
|
node.parentNode.removeChild(node);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Warn of string does not begin '${' and end '}'
|
|
* @param str the string to check.
|
|
* @return The string stripped of ${ and }, or untouched if it does not match
|
|
*/
|
|
Templater.prototype.stripBraces = function(str) {
|
|
if (!str.match(/\$\{.*\}/g)) {
|
|
this.handleError('Expected ' + str + ' to match ${...}');
|
|
return str;
|
|
}
|
|
return str.slice(2, -1);
|
|
};
|
|
|
|
/**
|
|
* Combined getter and setter that works with a path through some data set.
|
|
* For example:
|
|
* <ul>
|
|
* <li>property('a.b', { a: { b: 99 }}); // returns 99
|
|
* <li>property('a', { a: { b: 99 }}); // returns { b: 99 }
|
|
* <li>property('a', { a: { b: 99 }}, 42); // returns 99 and alters the
|
|
* input data to be { a: { b: 42 }}
|
|
* </ul>
|
|
* @param path An array of strings indicating the path through the data, or
|
|
* a string to be cut into an array using <tt>split('.')</tt>
|
|
* @param data An object to look in for the <tt>path</tt> argument
|
|
* @param newValue (optional) If defined, this value will replace the
|
|
* original value for the data at the path specified.
|
|
* @return The value pointed to by <tt>path</tt> before any
|
|
* <tt>newValue</tt> is applied.
|
|
*/
|
|
Templater.prototype.property = function(path, data, newValue) {
|
|
this.scope.push(path);
|
|
try {
|
|
if (typeof path === 'string') {
|
|
path = path.split('.');
|
|
}
|
|
var value = data[path[0]];
|
|
if (path.length === 1) {
|
|
if (newValue !== undefined) {
|
|
data[path[0]] = newValue;
|
|
}
|
|
if (typeof value === 'function') {
|
|
return function() {
|
|
return value.apply(data, arguments);
|
|
};
|
|
}
|
|
return value;
|
|
}
|
|
if (!value) {
|
|
this.handleError('Can\'t find path=' + path);
|
|
return null;
|
|
}
|
|
return this.property(path.slice(1), value, newValue);
|
|
} finally {
|
|
this.scope.pop();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Like eval, but that creates a context of the variables in <tt>env</tt> in
|
|
* which the script is evaluated.
|
|
* WARNING: This script uses 'with' which is generally regarded to be evil.
|
|
* The alternative is to create a Function at runtime that takes X parameters
|
|
* according to the X keys in the env object, and then call that function using
|
|
* the values in the env object. This is likely to be slow, but workable.
|
|
* @param script The string to be evaluated.
|
|
* @param env The environment in which to eval the script.
|
|
* @param context Optional debugging string in case of failure
|
|
* @return The return value of the script, or the error message if the script
|
|
* execution failed.
|
|
*/
|
|
Templater.prototype.envEval = function(script, env, context) {
|
|
with (env) {
|
|
try {
|
|
this.scope.push(context);
|
|
return eval(script);
|
|
} catch (ex) {
|
|
this.handleError('Template error evaluating \'' + script + '\'', ex);
|
|
return script;
|
|
} finally {
|
|
this.scope.pop();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A generic way of reporting errors, for easy overloading in different
|
|
* environments.
|
|
* @param message the error message to report.
|
|
* @param ex optional associated exception.
|
|
*/
|
|
Templater.prototype.handleError = function(message, ex) {
|
|
this.logError(message);
|
|
this.logError('In: ' + this.scope.join(' > '));
|
|
if (ex) {
|
|
this.logError(ex);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* A generic way of reporting errors, for easy overloading in different
|
|
* environments.
|
|
* @param message the error message to report.
|
|
*/
|
|
Templater.prototype.logError = function(message) {
|
|
window.console && window.console.log && console.log(message);
|
|
};
|
|
|
|
exports.Templater = Templater;
|
|
|
|
|
|
});
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Skywriter.
|
|
*
|
|
* The Initial Developer of the Original Code is
|
|
* Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2009
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Skywriter Team (skywriter@mozilla.com)
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
define('cockpit/commands/basic', ['require', 'exports', 'module' , 'pilot/canon'], function(require, exports, module) {
|
|
|
|
|
|
var canon = require('pilot/canon');
|
|
|
|
/**
|
|
* '!' command
|
|
*/
|
|
var bangCommandSpec = {
|
|
name: 'sh',
|
|
description: 'Execute a system command (requires server support)',
|
|
params: [
|
|
{
|
|
name: 'command',
|
|
type: 'text',
|
|
description: 'The string to send to the os shell.'
|
|
}
|
|
],
|
|
exec: function(env, args, request) {
|
|
var req = new XMLHttpRequest();
|
|
req.open('GET', '/exec?args=' + args.command, true);
|
|
req.onreadystatechange = function(ev) {
|
|
if (req.readyState == 4) {
|
|
if (req.status == 200) {
|
|
request.done('<pre>' + req.responseText + '</pre>');
|
|
}
|
|
}
|
|
};
|
|
req.send(null);
|
|
}
|
|
};
|
|
|
|
var canon = require('pilot/canon');
|
|
|
|
exports.startup = function(data, reason) {
|
|
canon.addCommand(bangCommandSpec);
|
|
};
|
|
|
|
exports.shutdown = function(data, reason) {
|
|
canon.removeCommand(bangCommandSpec);
|
|
};
|
|
|
|
|
|
});
|
|
define("text!cockpit/ui/cli_view.css", [], "" +
|
|
"#cockpitInput { padding-left: 16px; }" +
|
|
"" +
|
|
".cptOutput { overflow: auto; position: absolute; z-index: 999; display: none; }" +
|
|
"" +
|
|
".cptCompletion { padding: 0; position: absolute; z-index: -1000; }" +
|
|
".cptCompletion.VALID { background: #FFF; }" +
|
|
".cptCompletion.INCOMPLETE { background: #DDD; }" +
|
|
".cptCompletion.INVALID { background: #DDD; }" +
|
|
".cptCompletion span { color: #FFF; }" +
|
|
".cptCompletion span.INCOMPLETE { color: #DDD; border-bottom: 2px dotted #F80; }" +
|
|
".cptCompletion span.INVALID { color: #DDD; border-bottom: 2px dotted #F00; }" +
|
|
"span.cptPrompt { color: #66F; font-weight: bold; }" +
|
|
"" +
|
|
"" +
|
|
".cptHints {" +
|
|
" color: #000;" +
|
|
" position: absolute;" +
|
|
" border: 1px solid rgba(230, 230, 230, 0.8);" +
|
|
" background: rgba(250, 250, 250, 0.8);" +
|
|
" -moz-border-radius-topleft: 10px;" +
|
|
" -moz-border-radius-topright: 10px;" +
|
|
" border-top-left-radius: 10px; border-top-right-radius: 10px;" +
|
|
" z-index: 1000;" +
|
|
" padding: 8px;" +
|
|
" display: none;" +
|
|
"}" +
|
|
"" +
|
|
".cptFocusPopup { display: block; }" +
|
|
".cptFocusPopup.cptNoPopup { display: none; }" +
|
|
"" +
|
|
".cptHints ul { margin: 0; padding: 0 15px; }" +
|
|
"" +
|
|
".cptGt { font-weight: bold; font-size: 120%; }" +
|
|
"");
|
|
|
|
define("text!cockpit/ui/request_view.css", [], "" +
|
|
".cptRowIn {" +
|
|
" display: box; display: -moz-box; display: -webkit-box;" +
|
|
" box-orient: horizontal; -moz-box-orient: horizontal; -webkit-box-orient: horizontal;" +
|
|
" box-align: center; -moz-box-align: center; -webkit-box-align: center;" +
|
|
" color: #333;" +
|
|
" background-color: #EEE;" +
|
|
" width: 100%;" +
|
|
" font-family: consolas, courier, monospace;" +
|
|
"}" +
|
|
".cptRowIn > * { padding-left: 2px; padding-right: 2px; }" +
|
|
".cptRowIn > img { cursor: pointer; }" +
|
|
".cptHover { display: none; }" +
|
|
".cptRowIn:hover > .cptHover { display: block; }" +
|
|
".cptRowIn:hover > .cptHover.cptHidden { display: none; }" +
|
|
".cptOutTyped {" +
|
|
" box-flex: 1; -moz-box-flex: 1; -webkit-box-flex: 1;" +
|
|
" font-weight: bold; color: #000; font-size: 120%;" +
|
|
"}" +
|
|
".cptRowOutput { padding-left: 10px; line-height: 1.2em; }" +
|
|
".cptRowOutput strong," +
|
|
".cptRowOutput b," +
|
|
".cptRowOutput th," +
|
|
".cptRowOutput h1," +
|
|
".cptRowOutput h2," +
|
|
".cptRowOutput h3 { color: #000; }" +
|
|
".cptRowOutput a { font-weight: bold; color: #666; text-decoration: none; }" +
|
|
".cptRowOutput a: hover { text-decoration: underline; cursor: pointer; }" +
|
|
".cptRowOutput input[type=password]," +
|
|
".cptRowOutput input[type=text]," +
|
|
".cptRowOutput textarea {" +
|
|
" color: #000; font-size: 120%;" +
|
|
" background: transparent; padding: 3px;" +
|
|
" border-radius: 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px;" +
|
|
"}" +
|
|
".cptRowOutput table," +
|
|
".cptRowOutput td," +
|
|
".cptRowOutput th { border: 0; padding: 0 2px; }" +
|
|
".cptRowOutput .right { text-align: right; }" +
|
|
"");
|
|
|
|
define("text!cockpit/ui/request_view.html", [], "" +
|
|
"<div class=cptRow>" +
|
|
" <!-- The div for the input (i.e. what was typed) -->" +
|
|
" <div class=\"cptRowIn\" save=\"${rowin}\"" +
|
|
" onclick=\"${copyToInput}\"" +
|
|
" ondblclick=\"${executeRequest}\">" +
|
|
"" +
|
|
" <!-- What the user actually typed -->" +
|
|
" <div class=\"cptGt\">> </div>" +
|
|
" <div class=\"cptOutTyped\">${request.typed}</div>" +
|
|
"" +
|
|
" <!-- The extra details that appear on hover -->" +
|
|
" <div class=cptHover save=\"${duration}\"></div>" +
|
|
" <img class=cptHover onclick=\"${hideOutput}\" save=\"${hide}\"" +
|
|
" alt=\"Hide command output\" _src=\"${imageUrl('images/minus.png')}\"/>" +
|
|
" <img class=\"cptHover cptHidden\" onclick=\"${showOutput}\" save=\"${show}\"" +
|
|
" alt=\"Show command output\" _src=\"${imageUrl('images/plus.png')}\"/>" +
|
|
" <img class=cptHover onclick=\"${remove}\"" +
|
|
" alt=\"Remove this command from the history\"" +
|
|
" _src=\"${imageUrl('images/closer.png')}\"/>" +
|
|
"" +
|
|
" </div>" +
|
|
"" +
|
|
" <!-- The div for the command output -->" +
|
|
" <div class=\"cptRowOut\" save=\"${rowout}\">" +
|
|
" <div class=\"cptRowOutput\" save=\"${output}\"></div>" +
|
|
" <img _src=\"${imageUrl('images/throbber.gif')}\" save=\"${throb}\"/>" +
|
|
" </div>" +
|
|
"</div>" +
|
|
"");
|
|
|
|
define("text!cockpit/ui/images/closer.png", [], "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAj9JREFUeNp0ks+LUlEUx7/vV1o8Z8wUx3IEHcQmiBiQlomjRNCiZpEuEqF/oEUwq/6EhvoHggmRcJUQBM1CRJAW0aLIaGQimZJxJsWxyV/P9/R1zzWlFl04vPvOPZ9z7rnnK5imidmKRCIq+zxgdoPZ1T/ut8xeM3tcKpW6s1hhBkaj0Qj7bDebTX+324WmadxvsVigqipcLleN/d4rFoulORiLxTZY8ItOp8MBCpYkiYPj8Xjus9vtlORWoVB4KcTjcQc732dLpSRXvCZaAws6Q4WDdqsO52kNH+oCRFGEz+f7ydwBKRgMPmTXi49GI1x2D/DsznesB06ws2eDbI7w9HYN6bVjvGss4KAjwDAMq81mM2SW5Wa/3weBbz42UL9uYnVpiO2Nr9ANHSGXib2Wgm9tCYIggGKJEVkvlwgi5/FQRmTLxO6hgJVzI1x0T/fJrBtHJxPeL6tI/fsZLA6ot8lkQi8HRVbw94gkWYI5MaHrOjcCGSNRxZosy9y5cErDzn0Dqx7gcwO8WtBp4PndI35GMYqiUMUvBL5yOBz8yRfFNpbPmqgcCFh/IuHa1nR/YXGM8+oUpFhihEQiwcdRLpfVRqOBtWXWq34Gra6AXq8Hp2piZcmKT4cKnE4nwuHwdByVSmWQz+d32WCTlHG/qaHHREN9kgi0sYQfv0R4PB4EAgESQDKXy72fSy6VSnHJVatVf71eR7vd5n66mtfrRSgU4pLLZrOlf7RKK51Ok8g3/yPyR5lMZi7y3wIMAME4EigHWgKnAAAAAElFTkSuQmCC");
|
|
|
|
define("text!cockpit/ui/images/dot_clear.gif", [], "data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAEBMgA7");
|
|
|
|
define("text!cockpit/ui/images/minus.png", [], "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAAZiS0dEANIA0gDS7KbF4AAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9kFGw4xMrIJw5EAAAHcSURBVCjPhZIxSxtxGMZ/976XhJA/RA5EAyJcFksnp64hjUPBoXRyCYLQTyD0UxScu0nFwalCQSgFCVk7dXAwUAiBDA2RO4W7yN1x9+9gcyhU+pteHt4H3pfncay1LOl0OgY4BN4Ar/7KP4BvwNFwOIyWu87S2O12O8DxfD73oygiSRIAarUaxhhWV1fHwMFgMBiWxl6v9y6Koi+3t7ckSUKtVkNVAcjzvNRWVlYwxry9vLz86uzs7HjAZDKZGGstjUaDfxHHMSLC5ubmHdB2VfVwNpuZ5clxHPMcRVFwc3PTXFtbO3RFZHexWJCmabnweAaoVqvlv4vFAhHZdVX1ZZqmOI5DURR8fz/lxbp9Yrz+7bD72SfPcwBU1XdF5N5aWy2KgqIoeBzPEnWVLMseYnAcRERdVR27rrsdxzGqyutP6898+GBsNBqo6i9XVS88z9sOggAR4X94noeqXoiIHPm+H9XrdYIgIAxDwjAkTVPCMESzBy3LMprNJr7v34nIkV5dXd2fn59fG2P2siwjSRIqlQrWWlSVJFcqlQqtVot2u40xZu/s7OxnWbl+v98BjkejkT+dTgmCoDxtY2ODra2tMXBweno6fNJVgP39fQN8eKbkH09OTsqS/wHFRdHPfTSfjwAAAABJRU5ErkJggg==");
|
|
|
|
define("text!cockpit/ui/images/pinaction.png", [], "data:image/png;base64,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");
|
|
|
|
define("text!cockpit/ui/images/pinin.png", [], "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC7mlDQ1BJQ0MgUHJvZmlsZQAAeAGFVM9rE0EU/jZuqdAiCFprDrJ4kCJJWatoRdQ2/RFiawzbH7ZFkGQzSdZuNuvuJrWliOTi0SreRe2hB/+AHnrwZC9KhVpFKN6rKGKhFy3xzW5MtqXqwM5+8943731vdt8ADXLSNPWABOQNx1KiEWlsfEJq/IgAjqIJQTQlVdvsTiQGQYNz+Xvn2HoPgVtWw3v7d7J3rZrStpoHhP1A4Eea2Sqw7xdxClkSAog836Epx3QI3+PY8uyPOU55eMG1Dys9xFkifEA1Lc5/TbhTzSXTQINIOJT1cVI+nNeLlNcdB2luZsbIEL1PkKa7zO6rYqGcTvYOkL2d9H5Os94+wiHCCxmtP0a4jZ71jNU/4mHhpObEhj0cGDX0+GAVtxqp+DXCFF8QTSeiVHHZLg3xmK79VvJKgnCQOMpkYYBzWkhP10xu+LqHBX0m1xOv4ndWUeF5jxNn3tTd70XaAq8wDh0MGgyaDUhQEEUEYZiwUECGPBoxNLJyPyOrBhuTezJ1JGq7dGJEsUF7Ntw9t1Gk3Tz+KCJxlEO1CJL8Qf4qr8lP5Xn5y1yw2Fb3lK2bmrry4DvF5Zm5Gh7X08jjc01efJXUdpNXR5aseXq8muwaP+xXlzHmgjWPxHOw+/EtX5XMlymMFMXjVfPqS4R1WjE3359sfzs94i7PLrXWc62JizdWm5dn/WpI++6qvJPmVflPXvXx/GfNxGPiKTEmdornIYmXxS7xkthLqwviYG3HCJ2VhinSbZH6JNVgYJq89S9dP1t4vUZ/DPVRlBnM0lSJ93/CKmQ0nbkOb/qP28f8F+T3iuefKAIvbODImbptU3HvEKFlpW5zrgIXv9F98LZua6N+OPwEWDyrFq1SNZ8gvAEcdod6HugpmNOWls05Uocsn5O66cpiUsxQ20NSUtcl12VLFrOZVWLpdtiZ0x1uHKE5QvfEp0plk/qv8RGw/bBS+fmsUtl+ThrWgZf6b8C8/UXAeIuJAAAACXBIWXMAAAsTAAALEwEAmpwYAAABZ0lEQVQ4Ea2TPUsDQRCGZ89Eo4FACkULEQs1CH4Uamfjn7GxEYJFIFXgChFsbPwzNnZioREkaiHBQtEiEEiMRm/dZ8OEGAxR4sBxx877Pju7M2estTJIxLrNuVwuMxQEx0ZkzcFHyRtjXt02559RtB2GYanTYzoryOfz+6l4Nbszf2niwffKmpGRo9sVW22mDgqFwp5C2gDMm+P32a3JB1N+n5JifUGeP9JeNxGryPLYjcwMP8rJ07Q9fZltQzyAstOJ2vVu5sKc1ZZkRBrOcKeb+HexPidvkpCN5JUcllZtpZFc5DgBWc5M2eysZuMuofMBSA4NWjx4PUCsXefMlI0QY3ewRg4NWi4ZTQsgrjYXema+e4VqtEMK6KXvu+4B9Bklt90vVKMeD2BI6DOt4rZ/Gk7WyKFBi4fNPIAJY0joM61SCCZ9tI1o0OIB8D+DBIkYaJRbCBH9mZgNt+bb++ufSSF/eX8BYcDeAzuQJVUAAAAASUVORK5CYII=");
|
|
|
|
define("text!cockpit/ui/images/pinout.png", [], "data:image/png;base64,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");
|
|
|
|
define("text!cockpit/ui/images/pins.png", [], "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAQCAYAAABQrvyxAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGYklEQVRIDbVWe0yURxCf/R735o6DO0FBe0RFsaL4iLXGIKa2SY3P6JGa2GpjlJjUV9NosbU++tYUbEnaQIrVaKJBG7WiNFQFUWO1UUEsVg2CAgoeHHLewcH32O58cBdQsX9Y5+7LfrszOzO/2ZnZj1BKgTBiIwVGVvKd49OVVYunDlXn6wdBKh+ogXrv+DOz1melIb+3LM5fNv2XPYE5EHY+L3PJljN5zavHpJjsQNsA/JJEgyC2+WTjy3b0GfoJW8O4aoHtDwiHQrj5lw1LLyyb1bp5zAjJTus9klrVpdD6TqH2ngVO+0dsRJnp06cLIYU4fx7NnRI3bu7UIYOeJ/McnuY88q3k62gc0S4Dgf5qhICQtIXS2lqD7BhSduPk3YfyzXaANhBBJDxYdUqCywB2qS4RdyUuSkTF/VJxcbH5j8N7/75RuFrN3Zh8OS8zqf5m4UpPeenOyP42dbtBeuvVnCdkK1e4PfPouX03mo9se+c33M8wqDk5Ofqed8REUTicQhbySUxp9u3KlMSHTtrFU6Kyn03lz15PPpW25vsZeYSIKyiVURcqeZJOH9lTNZLfnxRjU/uwrjbEUBWsapcSO2Hq4k0VfZg9EzxdDNCEjDxgNqRDme9umz/btwlsHRIEePHgAf73RdnHZ6LTuIUBN7OBQ+c1Fdnp6cZ1BQUdeRuWZi97o3ktDQQkVeFFzqJARd1A5a0Vr7ta6Kp6TZjtZ+NTIOoKF6qDrL7e0QQIUCiqMMKk8Z1Q/SCSKvzocf2B6NEN0SQn/kTO6fKJ0zqjZUlQBSpJ0GjR77w0aoc1Pr6S5/kVJrNpakV5hR+LWKN4t7sLX+p0rx2vqSta64olIulUKUgCSXLWE1R4KPPSj+5vhm2hdDOG+CkQBmhhyyKq6SaFYWTn5bB3QJRNz54AuXKn8TJjhu0Wbv+wNEKQjVhnmKopjo4FxXmetCRnC4F7BhCiCUepqAepRh0TM/gjjzOOSK2NgWZPc05qampRWJHb7dbOffep2ednzLzgczlbrQA6gHYF9BYDh9GY+FjddMweHMscmMuep07gXlMQoqw9ALoYu5MJsak9QmJA2IvAgVmoCRciooyPujJtNCv1uHt3TmK9gegFKrG9kh6oXwZiIEAtBIjORGKNTWR/WeW8XVkbjuJepLAyloM8LmTN//njKZPbraATZaLjCHEww9Ei4FFiPg6Ja5gT6gxYgLgnRDHRQwJXbz2GOw0d4A3K4GXlUtMahJjYVxiYbrwOmxIS10bFnIBOSi6Tl9Jgs0zbOEX18wyEwgLPMrxD1Y4aCK8kmTpgYcpAF27Mzs42Hjx4kA8BICUlJfKArR7LcEvTB1xEC9AoEw9OPagWkVU/D1oesmK6U911zEczMVe01oZjiMggg6ux2Qk379qh4rYKet4GjrhhwEteBgBrH8BssoXEtbHzPpSBRRSpqlNpgAiUoxzHKxLRszoVuggIisxaDQWZqkQvQjAoax3NbDbLLGuUEABNGedXqSyLRupXgDT5JfAGZNLio9B0X8Uiwk4w77MDc1D4yejjWtykPS3DX01UDCY/GPQcVDe0QYT0CIxGFvUorfvBxZsRfVrUuWruMBAb/lXCUofoFNZfzGJtowXOX0vwUSFK4BgyMKm6P6s9wQUZld+jrYyMDC0iIQDaJdG4IyZQfL3RfbFcCBIlRgc+u3CjaTApuZ9KsANgG8PNzHlWWD3tCxd6kafNNiFp5HAalAkkJ0SCV2H3CgOD9Nc/FqrXuyb0Eocvfhq171p5eyuJ1omKJEP5rQGe/FOOnXtq335z8YmvYo9cHb2t8spIb3lVSseZW46FlGY/Sk9P50P2w20UlWJUkUHIushfc5PXGAzCo0PlD2pnpCYfCXga3lu+fPlevEhWrVrFyrN/Orfv87FOW9tlqb2Kc9pV8DzioMk3UNUbXM+8B/ATBr8C8CKdvGXWGD/9sqm3dkxtzA4McMjHMB8D2ftheYXo+qzt3pXvz8/PP/vk+v8537V+yYW87Zu+RZ1ZbrexoKAA/SBpaWn4+aL5w5zGk+/jW59JiMkESW5urpiVlWXENRb1H/Yf2I9txIxz5IdkX3TsraukpsbQjz6090yb4XsAvQoRE0YvJdamtIIbOnRoUVlZ2ftsLVQzIdEXHntsaZdimssVfCpFui109+BnWPsXaWLI/zactygAAAAASUVORK5CYII=");
|
|
|
|
define("text!cockpit/ui/images/plus.png", [], "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAAZiS0dEANIA0gDS7KbF4AAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9kFGw4yFTwuJTkAAAH7SURBVCjPdZKxa1NRFMZ/956XZMgFyyMlCZRA4hBx6lBcQ00GoYi4tEstFPwLAs7iLDi7FWuHThaUggihBDI5OWRoQAmBQFISQgvvpbwX3rsOaR4K+o2H8zvfOZxPWWtZqVarGaAJPAEe3ZW/A1+Bd+1221v1qhW4vb1dA44mk0nZ8zyCIAAgk8lgjGF9fb0PHF5cXLQTsF6vP/c879P19TVBEJDJZBARAKIoSmpra2sYY561Wq3PqtFouMBgMBgYay3ZbJZ/yfd9tNaUSqUboOKISPPq6sqsVvZ9H4AvL34B8PTj/QSO45jpdHovn883Ha31znw+JwzDpCEMQx4UloM8zyOdTif3zudztNY7jog8DMMQpRRxHPPt5TCBAEZvxlyOFTsfykRRBICIlB2t9a21Nh3HMXEc8+d7VhJHWCwWyzcohdZaHBHpO46z6fs+IsLj94XECaD4unCHL8FsNouI/HRE5Nx13c3ZbIbWOnG5HKtl+53TSq7rIiLnand31wUGnU7HjEYjlFLJZN/3yRnL1FMYY8jlcmxtbd0AFel2u7dnZ2eXxpi9xWJBEASkUimstYgIQSSkUimKxSKVSgVjzN7p6emPJHL7+/s14KjX65WHwyGz2SxZbWNjg2q12gcOT05O2n9lFeDg4MAAr/4T8rfHx8dJyH8DvvbYGzKvWukAAAAASUVORK5CYII=");
|
|
|
|
define("text!cockpit/ui/images/throbber.gif", [], "data:image/gif;base64,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");
|
|
|