blob: e4b4a858f7f3761970a33ad6470ed6b36dd3906d [file] [log] [blame]
/**@license
* __ _____ ________ __
* / // _ /__ __ _____ ___ __ _/__ ___/__ ___ ______ __ __ __ ___ / /
* __ / // // // // // _ // _// // / / // _ // _// // // \/ // _ \/ /
* / / // // // // // ___// / / // / / // ___// / / / / // // /\ // // / /__
* \___//____ \\___//____//_/ _\_ / /_//____//_/ /_/ /_//_//_/ /_/ \__\_\___/
* \/ /____/ version 0.10.12
*
* This file is part of jQuery Terminal. http://terminal.jcubic.pl
*
* Copyright (c) 2010-2016 Jakub Jankiewicz <http://jcubic.pl>
* Released under the MIT license
*
* Contains:
*
* Storage plugin Distributed under the MIT License
* Copyright (c) 2010 Dave Schindler
*
* jQuery Timers licenced with the WTFPL
* <http://jquery.offput.ca/timers/>
*
* Cross-Browser Split 1.1.1
* Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
* Available under the MIT License
*
* jQuery Caret
* Copyright (c) 2009, Gideon Sireling
* 3 clause BSD License
*
* sprintf.js
* Copyright (c) 2007-2013 Alexandru Marasteanu <hello at alexei dot ro>
* licensed under 3 clause BSD license
*
* Date: Thu, 16 Jun 2016 09:23:57 +0000
*/
/* TODO:
*
* Debug interpreters names in LocalStorage
* onPositionChange event add to terminal ???
* different command line history for each login users (add login if present to
* localStorage key)
*
* TEST: login + promises/exec
* json-rpc/object + promises
*
* NOTE: json-rpc don't need promises and delegate resume/pause because only
* exec can call it and exec call interpreter that work with resume/pause
*/
/* jshint ignore:start */
(function(ctx) {
var sprintf = function() {
if (!sprintf.cache.hasOwnProperty(arguments[0])) {
sprintf.cache[arguments[0]] = sprintf.parse(arguments[0]);
}
return sprintf.format.call(null, sprintf.cache[arguments[0]], arguments);
};
sprintf.format = function(parse_tree, argv) {
var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length;
for (i = 0; i < tree_length; i++) {
node_type = get_type(parse_tree[i]);
if (node_type === 'string') {
output.push(parse_tree[i]);
}
else if (node_type === 'array') {
match = parse_tree[i]; // convenience purposes only
if (match[2]) { // keyword argument
arg = argv[cursor];
for (k = 0; k < match[2].length; k++) {
if (!arg.hasOwnProperty(match[2][k])) {
throw(sprintf('[sprintf] property "%s" does not exist', match[2][k]));
}
arg = arg[match[2][k]];
}
}
else if (match[1]) { // positional argument (explicit)
arg = argv[match[1]];
}
else { // positional argument (implicit)
arg = argv[cursor++];
}
if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) {
throw(sprintf('[sprintf] expecting number but found %s', get_type(arg)));
}
switch (match[8]) {
case 'b': arg = arg.toString(2); break;
case 'c': arg = String.fromCharCode(arg); break;
case 'd': arg = parseInt(arg, 10); break;
case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break;
case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break;
case 'o': arg = arg.toString(8); break;
case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break;
case 'u': arg = arg >>> 0; break;
case 'x': arg = arg.toString(16); break;
case 'X': arg = arg.toString(16).toUpperCase(); break;
}
arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg);
pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' ';
pad_length = match[6] - String(arg).length;
pad = match[6] ? str_repeat(pad_character, pad_length) : '';
output.push(match[5] ? arg + pad : pad + arg);
}
}
return output.join('');
};
sprintf.cache = {};
sprintf.parse = function(fmt) {
var _fmt = fmt, match = [], parse_tree = [], arg_names = 0;
while (_fmt) {
if ((match = /^[^\x25]+/.exec(_fmt)) !== null) {
parse_tree.push(match[0]);
}
else if ((match = /^\x25{2}/.exec(_fmt)) !== null) {
parse_tree.push('%');
}
else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) {
if (match[2]) {
arg_names |= 1;
var field_list = [], replacement_field = match[2], field_match = [];
if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
field_list.push(field_match[1]);
while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
field_list.push(field_match[1]);
}
else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) {
field_list.push(field_match[1]);
}
else {
throw('[sprintf] huh?');
}
}
}
else {
throw('[sprintf] huh?');
}
match[2] = field_list;
}
else {
arg_names |= 2;
}
if (arg_names === 3) {
throw('[sprintf] mixing positional and named placeholders is not (yet) supported');
}
parse_tree.push(match);
}
else {
throw('[sprintf] huh?');
}
_fmt = _fmt.substring(match[0].length);
}
return parse_tree;
};
var vsprintf = function(fmt, argv, _argv) {
_argv = argv.slice(0);
_argv.splice(0, 0, fmt);
return sprintf.apply(null, _argv);
};
/**
* helpers
*/
function get_type(variable) {
return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();
}
function str_repeat(input, multiplier) {
for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */}
return output.join('');
}
/**
* export to either browser or node.js
*/
ctx.sprintf = sprintf;
ctx.vsprintf = vsprintf;
})(typeof global != "undefined" ? global : window);
/* jshint ignore:end */
(function($, undefined) {
"use strict";
// -----------------------------------------------------------------------
// :: map object to object
// -----------------------------------------------------------------------
$.omap = function(o, fn) {
var result = {};
$.each(o, function(k, v) {
result[k] = fn.call(o, k, v);
});
return result;
};
var Clone = {
clone_object: function(object) {
var tmp = {};
if (typeof object == 'object') {
if ($.isArray(object)) {
return this.clone_array(object);
} else if (object === null) {
return object;
} else {
for (var key in object) {
if ($.isArray(object[key])) {
tmp[key] = this.clone_array(object[key]);
} else if (typeof object[key] == 'object') {
tmp[key] = this.clone_object(object[key]);
} else {
tmp[key] = object[key];
}
}
}
}
return tmp;
},
clone_array: function(array) {
if (!$.isFunction(Array.prototype.map)) {
throw new Error("You'r browser don't support ES5 array map " +
"use es5-shim");
}
return array.slice(0).map(function(item) {
if (typeof item == 'object') {
return this.clone_object(item);
} else {
return item;
}
}.bind(this));
}
};
var clone = function(object) {
return Clone.clone_object(object);
};
var hasLS = function () {
var testKey = 'test', storage = window.localStorage;
try {
storage.setItem(testKey, '1');
storage.removeItem(testKey);
return true;
} catch (error) {
return false;
}
};
/* jshint ignore:start */
// -----------------------------------------------------------------------
// :: Storage plugin
// -----------------------------------------------------------------------
// Private data
var isLS = hasLS();
// Private functions
function wls(n, v) {
var c;
if (typeof n === 'string' && typeof v === 'string') {
localStorage[n] = v;
return true;
} else if (typeof n === 'object' && typeof v === 'undefined') {
for (c in n) {
if (n.hasOwnProperty(c)) {
localStorage[c] = n[c];
}
}
return true;
}
return false;
}
function wc(n, v) {
var dt, e, c;
dt = new Date();
dt.setTime(dt.getTime() + 31536000000);
e = '; expires=' + dt.toGMTString();
if (typeof n === 'string' && typeof v === 'string') {
document.cookie = n + '=' + v + e + '; path=/';
return true;
} else if (typeof n === 'object' && typeof v === 'undefined') {
for (c in n) {
if (n.hasOwnProperty(c)) {
document.cookie = c + '=' + n[c] + e + '; path=/';
}
}
return true;
}
return false;
}
function rls(n) {
return localStorage[n];
}
function rc(n) {
var nn, ca, i, c;
nn = n + '=';
ca = document.cookie.split(';');
for (i = 0; i < ca.length; i++) {
c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1, c.length);
}
if (c.indexOf(nn) === 0) {
return c.substring(nn.length, c.length);
}
}
return null;
}
function dls(n) {
return delete localStorage[n];
}
function dc(n) {
return wc(n, '', -1);
}
/**
* Public API
* $.Storage.set("name", "value")
* $.Storage.set({"name1":"value1", "name2":"value2", etc})
* $.Storage.get("name")
* $.Storage.remove("name")
*/
$.extend({
Storage: {
set: isLS ? wls : wc,
get: isLS ? rls : rc,
remove: isLS ? dls : dc
}
});
// -----------------------------------------------------------------------
// :: jQuery Timers
// -----------------------------------------------------------------------
var jQuery = $;
jQuery.fn.extend({
everyTime: function(interval, label, fn, times, belay) {
return this.each(function() {
jQuery.timer.add(this, interval, label, fn, times, belay);
});
},
oneTime: function(interval, label, fn) {
return this.each(function() {
jQuery.timer.add(this, interval, label, fn, 1);
});
},
stopTime: function(label, fn) {
return this.each(function() {
jQuery.timer.remove(this, label, fn);
});
}
});
jQuery.extend({
timer: {
guid: 1,
global: {},
regex: /^([0-9]+)\s*(.*s)?$/,
powers: {
// Yeah this is major overkill...
'ms': 1,
'cs': 10,
'ds': 100,
's': 1000,
'das': 10000,
'hs': 100000,
'ks': 1000000
},
timeParse: function(value) {
if (value === undefined || value === null) {
return null;
}
var result = this.regex.exec(jQuery.trim(value.toString()));
if (result[2]) {
var num = parseInt(result[1], 10);
var mult = this.powers[result[2]] || 1;
return num * mult;
} else {
return value;
}
},
add: function(element, interval, label, fn, times, belay) {
var counter = 0;
if (jQuery.isFunction(label)) {
if (!times) {
times = fn;
}
fn = label;
label = interval;
}
interval = jQuery.timer.timeParse(interval);
if (typeof interval !== 'number' ||
isNaN(interval) ||
interval <= 0) {
return;
}
if (times && times.constructor !== Number) {
belay = !!times;
times = 0;
}
times = times || 0;
belay = belay || false;
if (!element.$timers) {
element.$timers = {};
}
if (!element.$timers[label]) {
element.$timers[label] = {};
}
fn.$timerID = fn.$timerID || this.guid++;
var handler = function() {
if (belay && handler.inProgress) {
return;
}
handler.inProgress = true;
if ((++counter > times && times !== 0) ||
fn.call(element, counter) === false) {
jQuery.timer.remove(element, label, fn);
}
handler.inProgress = false;
};
handler.$timerID = fn.$timerID;
if (!element.$timers[label][fn.$timerID]) {
element.$timers[label][fn.$timerID] = window.setInterval(handler, interval);
}
if (!this.global[label]) {
this.global[label] = [];
}
this.global[label].push(element);
},
remove: function(element, label, fn) {
var timers = element.$timers, ret;
if (timers) {
if (!label) {
for (var lab in timers) {
if (timers.hasOwnProperty(lab)) {
this.remove(element, lab, fn);
}
}
} else if (timers[label]) {
if (fn) {
if (fn.$timerID) {
window.clearInterval(timers[label][fn.$timerID]);
delete timers[label][fn.$timerID];
}
} else {
for (var _fn in timers[label]) {
if (timers[label].hasOwnProperty(_fn)) {
window.clearInterval(timers[label][_fn]);
delete timers[label][_fn];
}
}
}
for (ret in timers[label]) {
if (timers[label].hasOwnProperty(ret)) {
break;
}
}
if (!ret) {
ret = null;
delete timers[label];
}
}
for (ret in timers) {
if (timers.hasOwnProperty(ret)) {
break;
}
}
if (!ret) {
element.$timers = null;
}
}
}
}
});
if (/(msie) ([\w.]+)/.exec(navigator.userAgent.toLowerCase())) {
jQuery(window).one('unload', function() {
var global = jQuery.timer.global;
for (var label in global) {
if (global.hasOwnProperty(label)) {
var els = global[label], i = els.length;
while (--i) {
jQuery.timer.remove(els[i], label);
}
}
}
});
}
// -----------------------------------------------------------------------
// :: CROSS BROWSER SPLIT
// -----------------------------------------------------------------------
(function(undef) {
// prevent double include
if (!String.prototype.split.toString().match(/\[native/)) {
return;
}
var nativeSplit = String.prototype.split,
compliantExecNpcg = /()??/.exec("")[1] === undef, // NPCG: nonparticipating capturing group
self;
self = function (str, separator, limit) {
// If `separator` is not a regex, use `nativeSplit`
if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
return nativeSplit.call(str, separator, limit);
}
var output = [],
flags = (separator.ignoreCase ? "i" : "") +
(separator.multiline ? "m" : "") +
(separator.extended ? "x" : "") + // Proposed for ES6
(separator.sticky ? "y" : ""), // Firefox 3+
lastLastIndex = 0,
// Make `global` and avoid `lastIndex` issues by working with a copy
separator2, match, lastIndex, lastLength;
separator = new RegExp(separator.source, flags + "g");
str += ""; // Type-convert
if (!compliantExecNpcg) {
// Doesn't need flags gy, but they don't hurt
separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
}
/* Values for `limit`, per the spec:
* If undefined: 4294967295 // Math.pow(2, 32) - 1
* If 0, Infinity, or NaN: 0
* If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
* If negative number: 4294967296 - Math.floor(Math.abs(limit))
* If other: Type-convert, then use the above rules
*/
// ? Math.pow(2, 32) - 1 : ToUint32(limit)
limit = limit === undef ? -1 >>> 0 : limit >>> 0;
while (match = separator.exec(str)) {
// `separator.lastIndex` is not reliable cross-browser
lastIndex = match.index + match[0].length;
if (lastIndex > lastLastIndex) {
output.push(str.slice(lastLastIndex, match.index));
// Fix browsers whose `exec` methods don't consistently return `undefined` for
// nonparticipating capturing groups
if (!compliantExecNpcg && match.length > 1) {
match[0].replace(separator2, function () {
for (var i = 1; i < arguments.length - 2; i++) {
if (arguments[i] === undef) {
match[i] = undef;
}
}
});
}
if (match.length > 1 && match.index < str.length) {
Array.prototype.push.apply(output, match.slice(1));
}
lastLength = match[0].length;
lastLastIndex = lastIndex;
if (output.length >= limit) {
break;
}
}
if (separator.lastIndex === match.index) {
separator.lastIndex++; // Avoid an infinite loop
}
}
if (lastLastIndex === str.length) {
if (lastLength || !separator.test("")) {
output.push("");
}
} else {
output.push(str.slice(lastLastIndex));
}
return output.length > limit ? output.slice(0, limit) : output;
};
// For convenience
String.prototype.split = function (separator, limit) {
return self(this, separator, limit);
};
return self;
})();
// -----------------------------------------------------------------------
// :: jQuery Caret
// -----------------------------------------------------------------------
$.fn.caret = function(pos) {
var target = this[0];
var isContentEditable = target.contentEditable === 'true';
//get
if (arguments.length == 0) {
//HTML5
if (window.getSelection) {
//contenteditable
if (isContentEditable) {
target.focus();
var range1 = window.getSelection().getRangeAt(0),
range2 = range1.cloneRange();
range2.selectNodeContents(target);
range2.setEnd(range1.endContainer, range1.endOffset);
return range2.toString().length;
}
//textarea
return target.selectionStart;
}
//IE<9
if (document.selection) {
target.focus();
//contenteditable
if (isContentEditable) {
var range1 = document.selection.createRange(),
range2 = document.body.createTextRange();
range2.moveToElementText(target);
range2.setEndPoint('EndToEnd', range1);
return range2.text.length;
}
//textarea
var pos = 0,
range = target.createTextRange(),
range2 = document.selection.createRange().duplicate(),
bookmark = range2.getBookmark();
range.moveToBookmark(bookmark);
while (range.moveStart('character', -1) !== 0) pos++;
return pos;
}
//not supported
return 0;
}
//set
if (pos == -1)
pos = this[isContentEditable? 'text' : 'val']().length;
//HTML5
if (window.getSelection) {
//contenteditable
if (isContentEditable) {
target.focus();
window.getSelection().collapse(target.firstChild, pos);
}
//textarea
else
target.setSelectionRange(pos, pos);
}
//IE<9
else if (document.body.createTextRange) {
var range = document.body.createTextRange();
range.moveToElementText(target);
range.moveStart('character', pos);
range.collapse(true);
range.select();
}
if (!isContentEditable)
target.focus();
return pos;
};
/* jshint ignore:end */
// -----------------------------------------------------------------------
// :: Split string to array of strings with the same length
// -----------------------------------------------------------------------
function str_parts(str, length) {
var result = [];
var len = str.length;
if (len < length) {
return [str];
} else if (length < 0) {
throw new Error('str_parts: length can\'t be negative'); // '
}
for (var i = 0; i < len; i += length) {
result.push(str.substring(i, i + length));
}
return result;
}
// -----------------------------------------------------------------------
// :: CYCLE DATA STRUCTURE
// -----------------------------------------------------------------------
function Cycle(init) {
var data = init ? [init] : [];
var pos = 0;
$.extend(this, {
get: function() {
return data;
},
rotate: function() {
if (data.length === 1) {
return data[0];
} else {
if (pos === data.length - 1) {
pos = 0;
} else {
++pos;
}
if (data[pos]) {
return data[pos];
} else {
this.rotate();
}
}
},
length: function() {
return data.length;
},
remove: function(index) {
delete data[index];
},
set: function(item) {
for (var i = data.length; i--;) {
if (data[i] === item) {
pos = i;
return;
}
}
this.append(item);
},
front: function() {
if (data.length) {
var index = pos;
while(!data[index]) {
index++;
}
return data[index];
}
},
append: function(item) {
data.push(item);
}
});
}
// -----------------------------------------------------------------------
// :: STACK DATA STRUCTURE
// -----------------------------------------------------------------------
function Stack(init) {
var data = init instanceof Array ? init : init ? [init] : [];
$.extend(this, {
map: function(fn) {
return $.map(data, fn);
},
size: function() {
return data.length;
},
pop: function() {
if (data.length === 0) {
return null;
} else {
var value = data[data.length - 1];
data = data.slice(0, data.length - 1);
return value;
}
},
push: function(value) {
data = data.concat([value]);
return value;
},
top: function() {
return data.length > 0 ? data[data.length - 1] : null;
},
clone: function() {
return new Stack(data.slice(0));
}
});
}
// -------------------------------------------------------------------------
// :: Serialize object myself (biwascheme or prototype library do something
// :: wicked with JSON serialization for Arrays)
// -------------------------------------------------------------------------
$.json_stringify = function(object, level) {
var result = '', i;
level = level === undefined ? 1 : level;
var type = typeof object;
switch (type) {
case 'function':
result += object;
break;
case 'boolean':
result += object ? 'true' : 'false';
break;
case 'object':
if (object === null) {
result += 'null';
} else if (object instanceof Array) {
result += '[';
var len = object.length;
for (i = 0; i < len - 1; ++i) {
result += $.json_stringify(object[i], level + 1);
}
result += $.json_stringify(object[len - 1], level + 1) + ']';
} else {
result += '{';
for (var property in object) {
if (object.hasOwnProperty(property)) {
result += '"' + property + '":' +
$.json_stringify(object[property], level + 1);
}
}
result += '}';
}
break;
case 'string':
var str = object;
var repl = {
'\\\\': '\\\\',
'"': '\\"',
'/': '\\/',
'\\n': '\\n',
'\\r': '\\r',
'\\t': '\\t'};
for (i in repl) {
if (repl.hasOwnProperty(i)) {
str = str.replace(new RegExp(i, 'g'), repl[i]);
}
}
result += '"' + str + '"';
break;
case 'number':
result += String(object);
break;
}
result += (level > 1 ? ',' : '');
// quick hacks below
if (level === 1) {
// fix last comma
result = result.replace(/,([\]}])/g, '$1');
}
// fix comma before array or object
return result.replace(/([\[{]),/g, '$1');
};
// -------------------------------------------------------------------------
// :: HISTORY CLASS
// -------------------------------------------------------------------------
function History(name, size) {
var enabled = true;
var storage_key = '';
if (typeof name === 'string' && name !== '') {
storage_key = name + '_';
}
storage_key += 'commands';
var data = $.Storage.get(storage_key);
data = data ? $.parseJSON(data) : [];
var pos = data.length-1;
$.extend(this, {
append: function(item) {
if (enabled) {
if (data[data.length-1] !== item) {
data.push(item);
if (size && data.length > size) {
data = data.slice(-size);
}
pos = data.length-1;
$.Storage.set(storage_key, $.json_stringify(data));
}
}
},
data: function() {
return data;
},
reset: function() {
pos = data.length-1;
},
last: function() {
return data[length-1];
},
end: function() {
return pos === data.length-1;
},
position: function() {
return pos;
},
current: function() {
return data[pos];
},
next: function() {
if (pos < data.length-1) {
++pos;
}
if (pos !== -1) {
return data[pos];
}
},
previous: function() {
var old = pos;
if (pos > 0) {
--pos;
}
if (old !== -1) {
return data[pos];
}
},
clear: function() {
data = [];
this.purge();
},
enabled: function() {
return enabled;
},
enable: function() {
enabled = true;
},
purge: function() {
$.Storage.remove(storage_key);
},
disable: function() {
enabled = false;
}
});
}
// -----------------------------------------------------------------------
var is_paste_supported = (function() {
var el = document.createElement('div');
el.setAttribute('onpaste', 'return;');
return typeof el.onpaste == "function";
})();
var first_cmd = true;
// -------------------------------------------------------------------------
// :: COMMAND LINE PLUGIN
// -------------------------------------------------------------------------
$.fn.cmd = function(options) {
var self = this;
var maybe_data = self.data('cmd');
if (maybe_data) {
return maybe_data;
}
self.addClass('cmd');
self.append('<span class="prompt"></span><span></span>' +
'<span class="cursor">&nbsp;</span><span></span>');
// on mobile the only way to hide textarea on desktop it's needed because
// textarea show up after focus
//self.append('<span class="mask"></mask>');
var clip = $('<textarea />').addClass('clipboard').appendTo(self);
if (options.width) {
self.width(options.width);
}
var num_chars; // calculated by draw_prompt
var prompt_len;
var prompt_node = self.find('.prompt');
var reverse_search = false;
var rev_search_str = '';
var reverse_search_position = null;
var backup_prompt;
var mask = options.mask || false;
var command = '';
var last_command;
// text from selection using CTRL+SHIFT+C (as in Xterm)
var selected_text = '';
var kill_text = ''; // text from command that kill part of the command
var position = 0;
var prompt;
var enabled;
var historySize = options.historySize || 60;
var name, history;
var cursor = self.find('.cursor');
var animation;
function mobile_focus() {
if (is_touch) {
var focus = clip.is(':focus');
if (enabled) {
if (!focus) {
clip.focus();
self.oneTime(10, function() {
clip.focus();
});
}
} else {
if (focus) {
clip.blur();
}
}
}
}
// on mobile you can't delete character if input is empty (event
// will not fire) so we fake text entry, we could just put dummy
// data but we put real command and position
function fake_mobile_entry() {
if (is_touch) {
// delay worked while experimenting
self.oneTime(10, function() {
clip.val(command);
self.oneTime(10, function() {
clip.caret(position);
});
});
}
}
// terminal animation don't work on andorid because they animate
// 2 properties
if ((support_animations && !is_android)) {
animation = function(toggle) {
if (toggle) {
cursor.addClass('blink');
} else {
cursor.removeClass('blink');
}
};
} else {
var animating = false;
animation = function(toggle) {
if (toggle && !animating) {
animating = true;
cursor.addClass('inverted');
self.everyTime(500, 'blink', blink);
} else if (animating && !toggle) {
animating = false;
self.stopTime('blink', blink);
cursor.removeClass('inverted');
}
};
}
// ---------------------------------------------------------------------
// :: Blinking cursor function
// ---------------------------------------------------------------------
function blink(i) {
cursor.toggleClass('inverted');
}
// ---------------------------------------------------------------------
// :: Set prompt for reverse search
// ---------------------------------------------------------------------
function draw_reverse_prompt() {
prompt = "(reverse-i-search)`" + rev_search_str + "': ";
draw_prompt();
}
// ---------------------------------------------------------------------
// :: Disable reverse search
// ---------------------------------------------------------------------
function clear_reverse_state() {
prompt = backup_prompt;
reverse_search = false;
reverse_search_position = null;
rev_search_str = '';
}
// ---------------------------------------------------------------------
// :: Search through command line history. If next is not defined or
// :: false it searches for the first item from the end. If true it
// :: search for the next item
// ---------------------------------------------------------------------
function reverse_history_search(next) {
var history_data = history.data();
var regex, save_string;
var len = history_data.length;
if (next && reverse_search_position > 0) {
len -= reverse_search_position;
}
if (rev_search_str.length > 0) {
for (var j=rev_search_str.length; j>0; j--) {
save_string = $.terminal.escape_regex(rev_search_str.substring(0, j));
regex = new RegExp(save_string);
for (var i=len; i--;) {
if (regex.test(history_data[i])) {
reverse_search_position = history_data.length - i;
self.position(history_data[i].indexOf(save_string));
self.set(history_data[i], true);
redraw();
if (rev_search_str.length !== j) {
rev_search_str = rev_search_str.substring(0, j);
draw_reverse_prompt();
}
return;
}
}
}
}
rev_search_str = ''; // clear if not found any
}
// ---------------------------------------------------------------------
// :: Recalculate number of characters in command line
// ---------------------------------------------------------------------
function change_num_chars() {
var W = self.width();
var w = cursor.width();
num_chars = Math.floor(W / w);
}
// ---------------------------------------------------------------------
// :: Split String that fit into command line where first line need to
// :: fit next to prompt (need to have less characters)
// ---------------------------------------------------------------------
function get_splited_command_line(string) {
var first = string.substring(0, num_chars - prompt_len);
var rest = string.substring(num_chars - prompt_len);
return [first].concat(str_parts(rest, num_chars));
}
// ---------------------------------------------------------------------
// :: Function that displays the command line. Split long lines and
// :: place cursor in the right place
// ---------------------------------------------------------------------
var redraw = (function(self) {
var before = cursor.prev();
var after = cursor.next();
// -----------------------------------------------------------------
// :: Draw line with the cursor
// -----------------------------------------------------------------
function draw_cursor_line(string, position) {
var len = string.length;
if (position === len) {
before.html($.terminal.encode(string));
cursor.html('&nbsp;');
after.html('');
} else if (position === 0) {
before.html('');
//fix for tilda in IE
cursor.html($.terminal.encode(string.slice(0, 1)));
//cursor.html($.terminal.encode(string[0]));
after.html($.terminal.encode(string.slice(1)));
} else {
var before_str = string.slice(0, position);
before.html($.terminal.encode(before_str));
//fix for tilda in IE
var c = string.slice(position, position + 1);
//cursor.html(string[position]);
cursor.html($.terminal.encode(c));
if (position === string.length - 1) {
after.html('');
} else {
after.html($.terminal.encode(string.slice(position + 1)));
}
}
}
function div(string) {
return '<div>' + $.terminal.encode(string) + '</div>';
}
// -----------------------------------------------------------------
// :: Display lines after the cursor
// -----------------------------------------------------------------
function lines_after(lines) {
var last_ins = after;
$.each(lines, function(i, line) {
last_ins = $(div(line)).insertAfter(last_ins).
addClass('clear');
});
}
// -----------------------------------------------------------------
// :: Display lines before the cursor
// -----------------------------------------------------------------
function lines_before(lines) {
$.each(lines, function(i, line) {
before.before(div(line));
});
}
var count = 0;
// -----------------------------------------------------------------
// :: Redraw function
// -----------------------------------------------------------------
return function() {
var string;
var str; // max 80 line helper
switch(typeof mask) {
case 'boolean':
string = mask ? command.replace(/./g, '*') : command;
break;
case 'string':
string = command.replace(/./g, mask);
break;
}
var i, first_len;
self.find('div').remove();
before.html('');
// long line
if (string.length > num_chars - prompt_len - 1 ||
string.match(/\n/)) {
var array;
var tabs = string.match(/\t/g);
var tabs_rm = tabs ? tabs.length * 3 : 0;
//quick tabulation hack
if (tabs) {
string = string.replace(/\t/g, '\x00\x00\x00\x00');
}
// command contains new line characters
if (string.match(/\n/)) {
var tmp = string.split("\n");
first_len = num_chars - prompt_len - 1;
// empty character after each line
for (i=0; i<tmp.length-1; ++i) {
tmp[i] += ' ';
}
// split first line
if (tmp[0].length > first_len) {
array = [tmp[0].substring(0, first_len)];
str = tmp[0].substring(first_len);
array = array.concat(str_parts(str, num_chars));
} else {
array = [tmp[0]];
}
// process rest of the lines
for (i=1; i<tmp.length; ++i) {
if (tmp[i].length > num_chars) {
array = array.concat(str_parts(tmp[i],
num_chars));
} else {
array.push(tmp[i]);
}
}
} else {
array = get_splited_command_line(string);
}
if (tabs) {
array = $.map(array, function(line) {
return line.replace(/\x00\x00\x00\x00/g, '\t');
});
}
first_len = array[0].length;
//cursor in first line
if (first_len === 0 && array.length === 1) {
// skip empty line
} else if (position < first_len) {
draw_cursor_line(array[0], position);
lines_after(array.slice(1));
} else if (position === first_len) {
before.before(div(array[0]));
draw_cursor_line(array[1], 0);
lines_after(array.slice(2));
} else {
var num_lines = array.length;
var offset = 0;
if (position < first_len) {
draw_cursor_line(array[0], position);
lines_after(array.slice(1));
} else if (position === first_len) {
before.before(div(array[0]));
draw_cursor_line(array[1], 0);
lines_after(array.slice(2));
} else {
var last = array.slice(-1)[0];
var from_last = string.length - position - tabs_rm;
var last_len = last.length;
var pos = 0;
if (from_last <= last_len) {
lines_before(array.slice(0, -1));
if (last_len === from_last) {
pos = 0;
} else {
pos = last_len-from_last;
}
draw_cursor_line(last, pos);
} else {
// in the middle
if (num_lines === 3) {
str = $.terminal.encode(array[0]);
before.before('<div>' + str + '</div>');
draw_cursor_line(array[1],
position-first_len-1);
str = $.terminal.encode(array[2]);
after.after('<div class="clear">' + str +
'</div>');
} else {
// more lines, cursor in the middle
var line_index;
var current;
pos = position;
for (i=0; i<array.length; ++i) {
var current_len = array[i].length;
if (pos > current_len) {
pos -= current_len;
} else {
break;
}
}
current = array[i];
line_index = i;
// cursor on first character in line
if (pos === current.length) {
pos = 0;
current = array[++line_index];
}
draw_cursor_line(current, pos);
lines_before(array.slice(0, line_index));
lines_after(array.slice(line_index+1));
}
}
}
}
} else {
if (string === '') {
before.html('');
cursor.html('&nbsp;');
after.html('');
} else {
draw_cursor_line(string, position);
}
}
};
})(self);
// ---------------------------------------------------------------------
// :: Draw prompt that can be a function or a string
// ---------------------------------------------------------------------
var draw_prompt = (function() {
function set(prompt) {
prompt_node.html($.terminal.format($.terminal.encode(prompt)));
prompt_len = prompt_node.text().length;
}
return function() {
switch (typeof prompt) {
case 'string':
set(prompt);
break;
case 'function':
prompt(set);
break;
}
};
})();
// ---------------------------------------------------------------------
// :: Paste content to terminal using hidden textarea
// ---------------------------------------------------------------------
function paste(e) {
e = e.originalEvent;
if (self.isenabled()) {
var clip = self.find('textarea');
if (!clip.is(':focus')) {
clip.focus();
}
var text;
if (window.clipboardData && window.clipboardData.getData) { // IE
text = window.clipboardData.getData('Text');
} else if (e.clipboardData && e.clipboardData.getData) {
text = e.clipboardData.getData('text/plain');
} else {
//wait until Browser insert text to textarea
cmd.oneTime(100, function() {
self.insert(clip.val());
clip.val('');
fake_mobile_entry();
});
}
if (text) {
self.insert(text);
clip.val('');
fake_mobile_entry();
}
}
}
var first_up_history = true;
// prevent_keypress - hack for Android that was inserting characters on
// backspace
var prevent_keypress = false;
var no_keypress;
// ---------------------------------------------------------------------
// :: Keydown Event Handler
// ---------------------------------------------------------------------
function keydown_event(e) {
var result, pos, len;
if (enabled) {
if ($.isFunction(options.keydown)) {
result = options.keydown(e);
if (result !== undefined) {
//prevent_keypress = true;
return result;
}
}
if (e.which !== 38 &&
!(e.which === 80 && e.ctrlKey)) {
first_up_history = true;
}
// arrows / Home / End / ENTER
if (reverse_search && (e.which === 35 || e.which === 36 ||
e.which === 37 || e.which === 38 ||
e.which === 39 || e.which === 40 ||
e.which === 13 || e.which === 27)) {
clear_reverse_state();
draw_prompt();
if (e.which === 27) { // ESC
self.set('');
}
redraw();
// finish reverse search and execute normal event handler
/* jshint validthis:true */
keydown_event.call(this, e);
} else if (e.altKey) {
// Chrome on Windows sets ctrlKey and altKey for alt
// need to check for alt first
//if (e.which === 18) { // press ALT
if (e.which === 68) { //ALT+D
self.set(command.slice(0, position) +
command.slice(position).
replace(/ *[^ ]+ *(?= )|[^ ]+$/, ''), true);
// chrome jump to address bar
return false;
}
return true;
} else if (e.keyCode === 13) { //enter
if (e.shiftKey) {
self.insert('\n');
} else {
if (history && command && !mask &&
($.isFunction(options.historyFilter) &&
options.historyFilter(command)) ||
(options.historyFilter instanceof RegExp &&
command.match(options.historyFilter)) ||
!options.historyFilter) {
history.append(command);
}
var tmp = command;
history.reset();
self.set('');
if (options.commands) {
options.commands(tmp);
}
if ($.isFunction(prompt)) {
draw_prompt();
}
}
} else if (e.which === 8) { //backspace
if (reverse_search) {
rev_search_str = rev_search_str.slice(0, -1);
draw_reverse_prompt();
} else {
if (command !== '' && position > 0) {
self['delete'](-1);
}
}
if (is_touch) {
return true; // mobile fix
}
} else if (e.which === 67 && e.ctrlKey && e.shiftKey) {
// CTRL+SHIFT+C
selected_text = get_selected_text();
} else if (e.which === 86 && e.ctrlKey && e.shiftKey) {
if (selected_text !== '') {
self.insert(selected_text);
}
} else if (e.which === 9 && !(e.ctrlKey || e.altKey)) { // TAB
self.insert('\t');
} else if (e.which === 46) {
//DELETE
self['delete'](1);
return;
} else if (history && (e.which === 38 && !e.ctrlKey) ||
(e.which === 80 && e.ctrlKey)) {
//UP ARROW or CTRL+P
if (first_up_history) {
last_command = command;
self.set(history.current());
} else {
self.set(history.previous());
}
first_up_history = false;
} else if (history && (e.which === 40 && !e.ctrlKey) ||
(e.which === 78 && e.ctrlKey)) {
//DOWN ARROW or CTRL+N
self.set(history.end() ? last_command : history.next());
} else if (e.which === 37 || (e.which === 66 && e.ctrlKey)) {
//CTRL+LEFT ARROW or CTRL+B
if (e.ctrlKey && e.which !== 66) {
len = position - 1;
pos = 0;
if (command[len] === ' ') {
--len;
}
for (var i = len; i > 0; --i) {
if (command[i] === ' ' && command[i+1] !== ' ') {
pos = i + 1;
break;
} else if (command[i] === '\n' &&
command[i+1] !== '\n') {
pos = i;
break;
}
}
self.position(pos);
} else {
//LEFT ARROW or CTRL+B
if (position > 0) {
self.position(-1, true);
redraw();
}
}
} else if (e.which === 82 && e.ctrlKey) { // CTRL+R
if (reverse_search) {
reverse_history_search(true);
} else {
backup_prompt = prompt;
draw_reverse_prompt();
last_command = command;
self.set('');
redraw();
reverse_search = true;
}
} else if (e.which == 71 && e.ctrlKey) { // CTRL+G
if (reverse_search) {
prompt = backup_prompt;
draw_prompt();
self.set(last_command);
redraw();
reverse_search = false;
rev_search_str = '';
}
} else if (e.which === 39 ||
(e.which === 70 && e.ctrlKey)) {
//RIGHT ARROW OR CTRL+F
if (e.ctrlKey && e.which !== 70) {
// jump to beginning or end of the word
if (command[position] === ' ') {
++position;
}
var re = /\S[\n\s]{2,}|[\n\s]+\S?/;
var match = command.slice(position).match(re);
if (!match || match[0].match(/^\s+$/)) {
self.position(command.length);
} else {
if (match[0][0] !== ' ') {
position += match.index + 1;
} else {
position += match.index + match[0].length - 1;
if (match[0][match[0].length-1] !== ' ') {
--position;
}
}
}
redraw();
} else {
if (position < command.length) {
self.position(1, true);
}
}
} else if (e.which === 123) { // F12 - Allow Firebug
return;
} else if (e.which === 36) { // HOME
self.position(0);
} else if (e.which === 35) { // END
self.position(command.length);
} else if (e.shiftKey && e.which == 45) { // Shift+Insert
clip.val(''); // so we get it before paste event
if (!is_paste_supported) {
paste();
} else {
clip.focus();
}
return;
} else if (e.ctrlKey || e.metaKey) {
if (e.which === 192) { // CMD+` switch browser window on Mac
return;
}
if (e.metaKey) {
if(e.which === 82) { // CMD+r page reload in Chrome Mac
return;
} else if(e.which === 76) {
// CMD+l jump into Omnibox on Chrome Mac
return;
}
}
if (e.shiftKey) { // CTRL+SHIFT+??
if (e.which === 84) {
//CTRL+SHIFT+T open closed tab
return;
}
//} else if (e.altKey) { //ALT+CTRL+??
} else {
if (e.which === 81) { // CTRL+W
// don't work in Chromium (can't prevent close tab)
if (command !== '' && position !== 0) {
var m = command.slice(0, position).match(/([^ ]+ *$)/);
kill_text = self['delete'](-m[0].length);
}
return false;
} else if (e.which === 72) { // CTRL+H
if (command !== '' && position > 0) {
self['delete'](-1);
}
return false;
//NOTE: in opera charCode is undefined
} else if (e.which === 65) {
//CTRL+A
self.position(0);
} else if (e.which === 69) {
//CTRL+E
self.position(command.length);
} else if (e.which === 88 || e.which === 67 ||
e.which === 84) {
//CTRL+X CTRL+C CTRL+W CTRL+T
return;
} else if (e.which === 89) { // CTRL+Y
if (kill_text !== '') {
self.insert(kill_text);
}
} else if (e.which === 86) { // CTRL+V
clip.val('');
if (!is_paste_supported) {
paste();
} else {
clip.focus();
}
return;
} else if (e.which === 75) { // CTRL+K
kill_text = self['delete'](command.length-position);
} else if (e.which === 85) { // CTRL+U
if (command !== '' && position !== 0) {
kill_text = self['delete'](-position);
}
} else if (e.which === 17) { //CTRL+TAB switch tab
return false;
}
}
} else {
prevent_keypress = false;
no_keypress = true;
return;
}
// this will prevent for instance backspace to go back one page
//prevent_keypress = true;
e.preventDefault();
}
}
function fire_change_command() {
if ($.isFunction(options.onCommandChange)) {
options.onCommandChange(command);
}
}
// ---------------------------------------------------------------------
// :: Command Line Methods
// ---------------------------------------------------------------------
$.extend(self, {
name: function(string) {
if (string !== undefined) {
name = string;
var enabled = history && history.enabled() || !history;
history = new History(string, historySize);
// disable new history if old was disabled
if (!enabled) {
history.disable();
}
return self;
} else {
return name;
}
},
purge: function() {
history.clear();
return self;
},
history: function() {
return history;
},
'delete': function(n, stay) {
var removed;
if (n === 0) {
return self;
} else if (n < 0) {
if (position > 0) {
// this may look weird but if n is negative we need
// to use +
removed = command.slice(0, position).slice(n);
command = command.slice(0, position + n) +
command.slice(position, command.length);
if (!stay) {
self.position(position+n);
} else {
fire_change_command();
}
}
} else {
if (command !== '' && position < command.length) {
removed = command.slice(position).slice(0, n);
command = command.slice(0, position) +
command.slice(position + n, command.length);
fire_change_command();
}
}
redraw();
fake_mobile_entry();
return removed;
},
set: function(string, stay) {
if (string !== undefined) {
command = string;
if (!stay) {
self.position(command.length);
}
redraw();
fake_mobile_entry();
fire_change_command();
}
return self;
},
insert: function(string, stay) {
if (position === command.length) {
command += string;
} else if (position === 0) {
command = string + command;
} else {
command = command.slice(0, position) +
string + command.slice(position);
}
if (!stay) {
self.position(string.length, true);
} else {
fake_mobile_entry();
}
redraw();
fire_change_command();
return self;
},
get: function() {
return command;
},
commands: function(commands) {
if (commands) {
options.commands = commands;
return self;
} else {
return commands;
}
},
destroy: function() {
doc.unbind('keypress.cmd', keypress_event);
doc.unbind('keydown.cmd', keydown_event);
doc.unbind('paste.cmd', paste);
doc.unbind('input.cmd', input);
self.stopTime('blink', blink);
self.find('.cursor').next().remove().end().prev().remove().
end().remove();
self.find('.prompt, .clipboard').remove();
self.removeClass('cmd').removeData('cmd');
return self;
},
prompt: function(user_prompt) {
if (user_prompt === undefined) {
return prompt;
} else {
if (typeof user_prompt === 'string' ||
typeof user_prompt === 'function') {
prompt = user_prompt;
} else {
throw new Error('prompt must be a function or string');
}
draw_prompt();
// we could check if command is longer then numchars-new
// prompt
redraw();
return self;
}
},
kill_text: function() {
return kill_text;
},
position: function(n, relative) {
if (typeof n === 'number') {
// if (position !== n) { this don't work, don't know why
if (relative) {
position += n;
} else {
if (n < 0) {
position = 0;
} else if (n > command.length) {
position = command.length;
} else {
position = n;
}
}
if ($.isFunction(options.onPositionChange)) {
options.onPositionChange(position);
}
redraw();
fake_mobile_entry();
return self;
} else {
return position;
}
},
visible: (function() {
var visible = self.visible;
return function() {
visible.apply(self, []);
redraw();
draw_prompt();
};
})(),
show: (function() {
var show = self.show;
return function() {
show.apply(self, []);
redraw();
draw_prompt();
};
})(),
resize: function(num) {
if (num) {
num_chars = num;
} else {
change_num_chars();
}
redraw();
return self;
},
enable: function() {
enabled = true;
self.addClass('enabled');
animation(true);
mobile_focus();
return self;
},
isenabled: function() {
return enabled;
},
disable: function() {
enabled = false;
self.removeClass('enabled');
animation(false);
mobile_focus();
return self;
},
mask: function(new_mask) {
if (typeof new_mask === 'undefined') {
return mask;
} else {
mask = new_mask;
redraw();
return self;
}
}
});
// ---------------------------------------------------------------------
// :: INIT
// ---------------------------------------------------------------------
self.name(options.name || options.prompt || '');
if (typeof options.prompt == 'string') {
prompt = options.prompt;
} else {
prompt = '> ';
}
draw_prompt();
if (options.enabled === undefined || options.enabled === true) {
self.enable();
}
// Keystrokes
var object;
var doc = $(document.documentElement || window);
function keypress_event(e) {
var result;
no_keypress = false;
if (e.ctrlKey && e.which === 99) { // CTRL+C
return;
}
if (prevent_keypress) {
return;
}
if (!reverse_search && $.isFunction(options.keypress)) {
result = options.keypress(e);
}
//$.terminal.active().echo(JSON.stringify(result));
if (result === undefined || result) {
if (enabled) {
if ($.inArray(e.which, [38, 13, 0, 8]) > -1 &&
//!(e.which === 40 && e.shiftKey ||
!(e.which === 38 && e.shiftKey)) {
if (e.keyCode == 123) { // for F12 which == 0
return;
}
return false;
} else if (!e.ctrlKey && !(e.altKey && e.which === 100) ||
e.altKey) { // ALT+D
if (reverse_search) {
rev_search_str += String.fromCharCode(e.which);
reverse_history_search();
draw_reverse_prompt();
} else {
self.insert(String.fromCharCode(e.which));
}
return false;
}
}
} else {
return result;
}
}
function input(e) {
if (no_keypress) {
// Some Androids don't fire keypress - #39
var val = clip.val();
if (val || e.which == 8) { // #209 ; 8 - backspace
self.set(val);
}
}
}
doc.bind('keypress.cmd', keypress_event).bind('keydown.cmd', keydown_event).
bind('input.cmd', input);
if (is_paste_supported) {
doc.bind('paste.cmd', paste);
}
// characters
self.data('cmd', self);
return self;
}; // cmd plugin
// -------------------------------------------------------------------------
// :: TOOLS
// -------------------------------------------------------------------------
function skip_formatting_count(string) {
// this will covert html entities to single characters
return $('<div>' + $.terminal.strip(string) + '</div>').text().length;
}
// -------------------------------------------------------------------------
function formatting_count(string) {
return string.length - skip_formatting_count(string);
}
// -------------------------------------------------------------------------
// taken from https://hacks.mozilla.org/2011/09/detecting-and-generating-
// css-animations-in-javascript/
var support_animations = (function() {
var animation = false,
animationstring = 'animation',
keyframeprefix = '',
domPrefixes = 'Webkit Moz O ms Khtml'.split(' '),
pfx = '',
elm = document.createElement('div');
if (elm.style.animationName) { animation = true; }
if (animation === false) {
for (var i = 0; i < domPrefixes.length; i++) {
var name = domPrefixes[i] + 'AnimationName';
if (elm.style[ name ] !== undefined) {
pfx = domPrefixes[i];
animationstring = pfx + 'Animation';
keyframeprefix = '-' + pfx.toLowerCase() + '-';
animation = true;
break;
}
}
}
return animation;
})();
// -------------------------------------------------------------------------
var is_android = navigator.userAgent.toLowerCase().indexOf("android") != -1;
// -------------------------------------------------------------------------
var is_touch = (function() {
return ('ontouchstart' in window) || window.DocumentTouch &&
document instanceof DocumentTouch;
})();
// -------------------------------------------------------------------------
function process_command(string, fn) {
var array = fn(string);
if (array.length) {
var name = array.shift();
var regex = new RegExp('^' + $.terminal.escape_regex(name));
var rest = string.replace(regex, '').trim();
return {
command: string,
name: name,
args: array,
rest: rest
};
} else {
return {
command: string,
name: '',
args: [],
rest: ''
};
}
}
// -------------------------------------------------------------------------
var format_split_re = /(\[\[[!gbiuso]*;[^;]*;[^\]]*\](?:[^\]]*\\\][^\]]*|[^\]]*|[^\[]*\[[^\]]*)\]?)/i;
var format_parts_re = /\[\[([!gbiuso]*);([^;]*);([^;\]]*);?([^;\]]*);?([^\]]*)\]([^\]]*\\\][^\]]*|[^\]]*|[^\[]*\[[^\]]*)\]?/gi;
var format_re = /\[\[([!gbiuso]*;[^;\]]*;[^;\]]*(?:;|[^\]()]*);?[^\]]*)\]([^\]]*\\\][^\]]*|[^\]]*|[^\[]*\[[^\]]*)\]?/gi;
var format_exist_re = /\[\[([!gbiuso]*;[^;\]]*;[^;\]]*(?:;|[^\]()]*);?[^\]]*)\]([^\]]*\\\][^\]]*|[^\]]*|[^\[]*\[[^\]]*)\]/gi;
var format_full_re = /^\[\[([!gbiuso]*;[^;\]]*;[^;\]]*(?:;|[^\]()]*);?[^\]]*)\]([^\]]*\\\][^\]]*|[^\]]*|[^\[]*\[[^\]]*)\]$/gi;
var color_hex_re = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
//var url_re = /https?:\/\/(?:(?!&[^;]+;)[^\s:"'<>)])+/g;
//var url_re = /\bhttps?:\/\/(?:(?!&[^;]+;)[^\s"'<>)])+\b/g;
var url_re = /(\bhttps?:\/\/(?:(?:(?!&[^;]+;)|(?=&amp;))[^\s"'<>\]\[)])+\b)/gi;
var url_nf_re = /\b(https?:\/\/(?:(?:(?!&[^;]+;)|(?=&amp;))[^\s"'<>\][)])+)\b(?![^[\]]*])/gi;
var email_re = /((([^<>('")[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,})))/g;
var command_re = /('[^']*'|"(\\"|[^"])*"|(?:\/(\\\/|[^\/])+\/[gimy]*)(?=:? |$)|(\\ |[^ ])+|[\w-]+)/gi;
var format_begin_re = /(\[\[[!gbiuso]*;[^;]*;[^\]]*\])/i;
var format_start_re = /^(\[\[[!gbiuso]*;[^;]*;[^\]]*\])/i;
var format_last_re = /\[\[[!gbiuso]*;[^;]*;[^\]]*\]?$/i;
var format_exec_re = /(\[\[(?:[^\]]|\\\])*\]\])/;
$.terminal = {
version: '0.10.12',
// colors from http://www.w3.org/wiki/CSS/Properties/color/keywords
color_names: [
'black', 'silver', 'gray', 'white', 'maroon', 'red', 'purple',
'fuchsia', 'green', 'lime', 'olive', 'yellow', 'navy', 'blue',
'teal', 'aqua', 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine',
'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue',
'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse',
'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson',
'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray',
'darkgreen', 'darkgrey', 'darkkhaki', 'darkmagenta',
'darkolivegreen', 'darkorange', 'darkorchid', 'darkred',
'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray',
'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink',
'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick',
'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite',
'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'grey',
'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki',
'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon',
'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow',
'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon',
'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey',
'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen',
'magenta', 'maroon', 'mediumaquamarine', 'mediumblue',
'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue',
'mediumspringgreen', 'mediumturquoise', 'mediumvioletred',
'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite',
'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered',
'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise',
'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum',
'powderblue', 'purple', 'red', 'rosybrown', 'royalblue',
'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell',
'sienna', 'silver', 'skyblue', 'slateblue', 'slategray',
'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', 'teal',
'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white',
'whitesmoke', 'yellow', 'yellowgreen'],
// ---------------------------------------------------------------------
// :: Validate html color (it can be name or hex)
// ---------------------------------------------------------------------
valid_color: function(color) {
if (color.match(color_hex_re)) {
return true;
} else {
return $.inArray(color.toLowerCase(),
$.terminal.color_names) !== -1;
}
},
// ---------------------------------------------------------------------
// :: Escape all special regex characters, so it can be use as regex to
// :: match exact string that contain those characters
// ---------------------------------------------------------------------
escape_regex: function(str) {
if (typeof str == 'string') {
var special = /([-\\\^$\[\]()+{}?*.|])/g;
return str.replace(special, '\\$1');
}
},
// ---------------------------------------------------------------------
// :: test if string contain formatting
// ---------------------------------------------------------------------
have_formatting: function(str) {
return typeof str == 'string' && !!str.match(format_exist_re);
},
is_formatting: function(str) {
return typeof str == 'string' && !!str.match(format_full_re);
},
// ---------------------------------------------------------------------
// :: return array of formatting and text between them
// ---------------------------------------------------------------------
format_split: function(str) {
return str.split(format_split_re);
},
// ---------------------------------------------------------------------
// :: split text into lines with equal length so each line can be
// :: rendered separately (text formatting can be longer then a line).
// ---------------------------------------------------------------------
split_equal: function(str, length, words) {
var formatting = false;
var in_text = false;
var prev_format = '';
var result = [];
// add format text as 5th paramter to formatting it's used for
// data attribute in format function
var array = str.replace(format_re, function(_, format, text) {
var semicolons = format.match(/;/g).length;
// missing semicolons
if (semicolons >= 4) {
return _;
} else if (semicolons == 2) {
semicolons = ';;';
} else if (semicolons == 3) {
semicolons = ';';
} else {
semicolons = '';
}
// return '[[' + format + ']' + text + ']';
// closing braket will break formatting so we need to escape
// those using html entity equvalent
var safe = text.replace(/\\\]/g, '&#93;').replace(/\n/g, '\\n').
replace(/&nbsp;/g, ' ');
return '[[' + format + semicolons + safe + ']' + text + ']';
}).split(/\n/g);
function is_space() {
return line.substring(j-6, j) == '&nbsp;' ||
line.substring(j-1, j) == ' ';
}
for (var i = 0, len = array.length; i < len; ++i) {
if (array[i] === '') {
result.push('');
continue;
}
var line = array[i];
var first_index = 0;
var count = 0;
var space = -1;
for (var j=0, jlen=line.length; j<jlen; ++j) {
if (line.substring(j).match(format_start_re)) {
formatting = true;
in_text = false;
} else if (formatting && line[j] === ']') {
if (in_text) {
formatting = false;
in_text = false;
} else {
in_text = true;
}
} else if ((formatting && in_text) || !formatting) {
if (line[j] === '&') { // treat entity as one character
var m = line.substring(j).match(/^(&[^;]+;)/);
if (!m) {
// should never happen if used by terminal,
// because it always calls $.terminal.encode
// before this function
throw new Error("Unclosed html entity in line " +
(i+1) + ' at char ' + (j+1));
}
j+=m[1].length-2; // because continue adds 1 to j
// if entity is at the end there is no next loop
// issue #77
if (j === jlen-1) {
result.push(output + m[1]);
}
continue;
} else if (line[j] === ']' && line[j-1] === '\\') {
// escape \] counts as one character
--count;
} else {
++count;
}
}
if (is_space() && ((formatting && in_text) || !formatting ||
(line[j] === '[' && line[j+1] === '['))) {
space = j;
}
if ((count === length || j === jlen-1) &&
((formatting && in_text) || !formatting)) {
var output;
var text = $.terminal.strip(line.substring(space));
text = $('<span>' + text + '</span>').text();
var text_len = text.length;
text = text.substring(0, j+length+1);
var can_break = !!text.match(/\s/) || j+length+1 > text_len;
if (words && space != -1 && j !== jlen-1 && can_break) {
output = line.substring(first_index, space);
j = space-1;
} else {
output = line.substring(first_index, j+1);
}
if (words) {
output = output.replace(/(&nbsp;|\s)+$/g, '');
}
space = -1;
first_index = j+1;
count = 0;
if (prev_format) {
output = prev_format + output;
if (output.match(']')) {
prev_format = '';
}
}
// Fix output if formatting not closed
var matched = output.match(format_re);
if (matched) {
var last = matched[matched.length-1];
if (last[last.length-1] !== ']') {
prev_format = last.match(format_begin_re)[1];
output += ']';
} else if (output.match(format_last_re)) {
var line_len = output.length;
// why this line ???
//var f_len = line_len-last[last.length-1].length;
output = output.replace(format_last_re, '');
prev_format = last.match(format_begin_re)[1];
}
}
result.push(output);
}
}
}
return result;
},
// ---------------------------------------------------------------------
// :: Encode formating as html for insertion into DOM
// ---------------------------------------------------------------------
encode: function(str) {
// don't escape entities
str = str.replace(/&(?!#[0-9]+;|[a-zA-Z]+;)/g, '&amp;');
return str.replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/ /g, '&nbsp;')
.replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
},
// ---------------------------------------------------------------------
// :: safe function that will render text as it is
// ---------------------------------------------------------------------
escape_formatting: function(string) {
return $.terminal.escape_brackets($.terminal.encode(string));
},
// ---------------------------------------------------------------------
// :: Replace terminal formatting with html
// ---------------------------------------------------------------------
format: function(str, options) {
var settings = $.extend({}, {
linksNoReferrer: false
}, options || {});
if (typeof str === 'string') {
//support for formating foo[[u;;]bar]baz[[b;#fff;]quux]zzz
var splitted = $.terminal.format_split(str);
str = $.map(splitted, function(text) {
if (text === '') {
return text;
} else if ($.terminal.is_formatting(text)) {
return text.replace(format_parts_re, function(s,
style,
color,
background,
_class,
data_text,
text) {
if (text === '') {
return ''; //'<span>&nbsp;</span>';
}
text = text.replace(/\\]/g, ']');
var style_str = '';
if (style.indexOf('b') !== -1) {
style_str += 'font-weight:bold;';
}
var text_decoration = [];
if (style.indexOf('u') !== -1) {
text_decoration.push('underline');
}
if (style.indexOf('s') !== -1) {
text_decoration.push('line-through');
}
if (style.indexOf('o') !== -1) {
text_decoration.push('overline');
}
if (text_decoration.length) {
style_str += 'text-decoration:' +
text_decoration.join(' ') + ';';
}
if (style.indexOf('i') !== -1) {
style_str += 'font-style:italic;';
}
if ($.terminal.valid_color(color)) {
style_str += 'color:' + color + ';';
if (style.indexOf('g') !== -1) {
style_str += 'text-shadow:0 0 5px ' + color + ';';
}
}
if ($.terminal.valid_color(background)) {
style_str += 'background-color:' + background;
}
var data;
if (data_text === '') {
data = text;
} else {
data = data_text.replace(/&#93;/g, ']');
}
var result;
if (style.indexOf('!') !== -1) {
if (data.match(email_re)) {
result = '<a href="mailto:' + data + '" ';
} else {
result = '<a target="_blank" href="' + data + '" ';
if (settings.linksNoReferrer) {
result += 'rel="noreferrer" ';
}
}
} else {
result = '<span ';
}
if (style_str !== '') {
result += 'style="' + style_str + '"';
}
if (_class !== '') {
result += ' class="' + _class + '"';
}
if (style.indexOf('!') !== -1) {
result += '>' + text + '</a>';
} else {
result += ' data-text="' +
data.replace('"', '&quote;') + '">' +
text + '</span>';
}
return result;
});
} else {
return '<span>' + text.replace(/\\\]/g, ']') + '</span>';
}
}).join('');
return str.replace(/<span><br\s*\/?><\/span>/gi, '<br/>');
} else {
return '';
}
},
// ---------------------------------------------------------------------
// :: Replace brackets with html entities
// ---------------------------------------------------------------------
escape_brackets: function(string) {
return string.replace(/\[/g, '&#91;').replace(/\]/g, '&#93;');
},
// ---------------------------------------------------------------------
// :: Remove formatting from text
// ---------------------------------------------------------------------
strip: function(str) {
return str.replace(format_parts_re, '$6');
},
// ---------------------------------------------------------------------
// :: Return active terminal
// ---------------------------------------------------------------------
active: function() {
return terminals.front();
},
// keep old as backward compatible
parseArguments: function(string) {
return $.terminal.parse_arguments(string);
},
splitArguments: function(string) {
return $.terminal.split_arguments(string);
},
parseCommand: function(string) {
return $.terminal.parse_command(string);
},
splitCommand: function(string) {
return $.terminal.split_command(string);
},
// ---------------------------------------------------------------------
// :: Function splits arguments and works with strings like
// :: 'asd' 'asd\' asd' "asd asd" asd\ 123 -n -b / [^ ]+ / /\s+/ asd\ a
// :: it creates a regex and numbers and replaces escape characters in
// :: double quotes
// ---------------------------------------------------------------------
parse_arguments: function(string) {
var float_re = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/;
return $.map(string.match(command_re) || [], function(arg) {
if (arg[0] === "'" && arg[arg.length-1] === "'") {
return arg.replace(/^'|'$/g, '');
} else if (arg[0] === '"' && arg[arg.length-1] === '"') {
arg = arg.replace(/^"|"$/g, '').replace(/\\([" ])/g, '$1');
return arg.replace(/\\\\|\\t|\\n/g, function(string) {
if (string[1] === 't') {
return '\t';
} else if (string[1] === 'n') {
return '\n';
} else {
return '\\';
}
}).replace(/\\x([0-9a-f]+)/gi, function(_, hex) {
return String.fromCharCode(parseInt(hex, 16));
}).replace(/\\0([0-7]+)/g, function(_, oct) {
return String.fromCharCode(parseInt(oct, 8));
});
} else if (arg.match(/^\/(\\\/|[^\/])+\/[gimy]*$/)) { // RegEx
var m = arg.match(/^\/([^\/]+)\/([^\/]*)$/);
return new RegExp(m[1], m[2]);
} else if (arg.match(/^-?[0-9]+$/)) {
return parseInt(arg, 10);
} else if (arg.match(float_re)) {
return parseFloat(arg);
} else {
return arg.replace(/\\ /g, ' ');
}
});
},
// ---------------------------------------------------------------------
// :: Split arguments: it only strips single and double quotes and
// :: escapes spaces
// ---------------------------------------------------------------------
split_arguments: function(string) {
return $.map(string.match(command_re) || [], function(arg) {
if (arg[0] === "'" && arg[arg.length-1] === "'") {
return arg.replace(/^'|'$/g, '');
} else if (arg[0] === '"' && arg[arg.length-1] === '"') {
return arg.replace(/^"|"$/g, '').replace(/\\([" ])/g, '$1');
} else if (arg.match(/\/.*\/[gimy]*$/)) {
return arg;
} else {
return arg.replace(/\\ /g, ' ');
}
});
},
// ---------------------------------------------------------------------
// :: Function that returns an object {name,args}. Arguments are parsed
// :: using the function parse_arguments
// ---------------------------------------------------------------------
parse_command: function(string) {
return process_command(string, $.terminal.parse_arguments);
},
// ---------------------------------------------------------------------
// :: Same as parse_command but arguments are parsed using split_arguments
// ---------------------------------------------------------------------
split_command: function(string) {
return process_command(string, $.terminal.split_arguments);
},
// ---------------------------------------------------------------------
// :: function executed for each text inside [{ .... }]
// ---------------------------------------------------------------------
extended_command: function(term, string) {
try {
change_hash = false;
term.exec(string, true).then(function() {
change_hash = true;
});
} catch(e) {
// error is process in exec
}
}
};
// -----------------------------------------------------------------------
// Helper plugins
// -----------------------------------------------------------------------
$.fn.visible = function() {
return this.css('visibility', 'visible');
};
$.fn.hidden = function() {
return this.css('visibility', 'hidden');
};
// -----------------------------------------------------------------------
// JSON-RPC CALL
// -----------------------------------------------------------------------
var ids = {}; // list of url based id of JSON-RPC
$.jrpc = function(url, method, params, success, error) {
ids[url] = ids[url] || 0;
var request = $.json_stringify({
'jsonrpc': '2.0', 'method': method,
'params': params, 'id': ++ids[url]});
return $.ajax({
url: url,
data: request,
success: function(result, status, jqXHR) {
var content_type = jqXHR.getResponseHeader('Content-Type');
if (!content_type.match(/application\/json/)) {
var msg = 'Response Content-Type is not application/json';
if (console && console.warn) {
console.warn(msg);
} else {
throw new Error('WARN: ' + msg);
}
}
var json;
try {
json = $.parseJSON(result);
} catch (e) {
if (error) {
error(jqXHR, 'Invalid JSON', e);
} else {
throw new Error('Invalid JSON');
}
return;
}
// don't catch errors in success callback
success(json, status, jqXHR);
},
error: error,
contentType: 'application/json',
dataType: 'text',
async: true,
cache: false,
//timeout: 1,
type: 'POST'});
};
// -----------------------------------------------------------------------
function is_scrolled_into_view(elem) {
var docViewTop = $(window).scrollTop();
var docViewBottom = docViewTop + $(window).height();
var elemTop = $(elem).offset().top;
var elemBottom = elemTop + $(elem).height();
return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom));
}
// -----------------------------------------------------------------------
// :: Create fake terminal to calcualte the dimention of one character
// :: this will make terminal work if terminal div is not added to the
// :: DOM at init like with:
// :: $('<div/>').terminal().echo('foo bar').appendTo('body');
// -----------------------------------------------------------------------
function char_size() {
var temp = $('<div class="terminal temp"><div class="cmd"><span cla' +
'ss="cursor">&nbsp;</span></div></div>').appendTo('body');
var span = temp.find('span');
var result = {
width: span.width(),
height: span.outerHeight()
};
temp.remove();
return result;
}
// -----------------------------------------------------------------------
// :: calculate numbers of characters
// -----------------------------------------------------------------------
function get_num_chars(terminal) {
var temp = $('<div class="terminal"><span class="cursor">' +
'</span></div>').appendTo('body').css('padding', 0);
var span = temp.find('span');
// use more characters to get width of single character as a fraction
var max = 60;
var spaces = '';
for (var i=0;i<=max; ++i) {
spaces += '&nbsp;';
}
span.html(spaces);
var width = span.width()/max;
var result = Math.floor(terminal.width() / width);
temp.remove();
if (have_scrollbars(terminal)) {
var SCROLLBAR_WIDTH = 20;
// assume that scrollbars are 20px - in my Laptop with
// Linux/Chrome they are 16px
var margins = terminal.innerWidth() - terminal.width();
result -= Math.ceil((SCROLLBAR_WIDTH - margins / 2) / (width-1));
}
return result;
}
// -----------------------------------------------------------------------
// :: Calculate number of lines that fit without scroll
// -----------------------------------------------------------------------
function get_num_rows(terminal) {
return Math.floor(terminal.height() / char_size().height);
}
// -----------------------------------------------------------------------
// :: Get Selected Text (this is internal because it return text even if
// :: it's outside of terminal, is used to paste text to the terminal)
// -----------------------------------------------------------------------
function get_selected_text() {
if (window.getSelection || document.getSelection) {
var selection = (window.getSelection || document.getSelection)();
if (selection.text) {
return selection.text;
} else {
return selection.toString();
}
} else if (document.selection) {
return document.selection.createRange().text;
}
}
// -----------------------------------------------------------------------
// :: check if div have scrollbars (need to have overflow auto or always)
// -----------------------------------------------------------------------
function have_scrollbars(div) {
if (div.css('overflow') == 'scroll' ||
div.css('overflow-y') == 'scroll') {
return true;
} else if (div.is('body')) {
return $("body").height() > $(window).height();
} else {
return div.get(0).scrollHeight > div.innerHeight();
}
}
// -----------------------------------------------------------------------
// :: TERMINAL PLUGIN CODE
// -----------------------------------------------------------------------
var version_set = !$.terminal.version.match(/^\{\{/);
var copyright = 'Copyright (c) 2011-2016 Jakub Jankiewicz <http://jcubic'+
'.pl>';
var version_string = version_set ? ' v. ' + $.terminal.version : ' ';
//regex is for placing version string aligned to the right
var reg = new RegExp(" {" + version_string.length + "}$");
var name_ver = 'jQuery Terminal Emulator' +
(version_set ? version_string : '');
// -----------------------------------------------------------------------
// :: Terminal Signatures
// -----------------------------------------------------------------------
var signatures = [
['jQuery Terminal', '(c) 2011-2016 jcubic'],
[name_ver, copyright.replace(/^Copyright | *<.*>/g, '')],
[name_ver, copyright.replace(/^Copyright /, '')],
[' _______ ________ __',
' / / _ /_ ____________ _/__ ___/______________ _____ / /',
' __ / / // / // / _ / _/ // / / / _ / _/ / / \\/ / _ \\/ /',
'/ / / // / // / ___/ // // / / / ___/ // / / / / /\\ / // / /__',
'\\___/____ \\\\__/____/_/ \\__ / /_/____/_//_/ /_/ /_/ \\/\\__\\_\\___/',
' \\/ /____/ '.replace(reg, ' ') +
version_string,
copyright],
[' __ _____ ________ __',
' / // _ /__ __ _____ ___ __ _/__ ___/__ ___ ______ __ __ __ ___ / /',
' __ / // // // // // _ // _// // / / // _ // _// // // \\/ // _ \\/ /',
'/ / // // // // // ___// / / // / / // ___// / / / / // // /\\ // // / /__',
'\\___//____ \\\\___//____//_/ _\\_ / /_//____//_/ /_/ /_//_//_/ /_/ \\__\\_\\___/',
' \\/ /____/ '.replace(reg, '') +
version_string,
copyright]
];
// -----------------------------------------------------------------------
// :: Default options
// -----------------------------------------------------------------------
$.terminal.defaults = {
prompt: '> ',
history: true,
exit: true,
clear: true,
enabled: true,
historySize: 60,
maskChar: '*',
checkArity: true,
raw: false,
exceptionHandler: null,
cancelableAjax: true,
processArguments: true,
linksNoReferrer: false,
processRPCResponse: null,
Token: true, // where this came from?
convertLinks: true,
historyState: false,
login: null,
outputLimit: -1,
formatters: [],
onAjaxError: null,
onRPCError: null,
completion: false,
historyFilter: null,
onInit: $.noop,
onClear: $.noop,
onBlur: $.noop,
onFocus: $.noop,
onTerminalChange: $.noop,
onExit: $.noop,
keypress: $.noop,
keydown: $.noop,
strings: {
wrongPasswordTryAgain: "Wrong password try again!",
wrongPassword: "Wrong password!",
ajaxAbortError: "Error while aborting ajax call!",
wrongArity: "Wrong number of arguments. Function '%s' expects %s got"+
" %s!",
commandNotFound: "Command '%s' Not Found!",
oneRPCWithIgnore: "You can use only one rpc with ignoreSystemDescr"+
"ibe",
oneInterpreterFunction: "You can't use more than one function (rpc"+
"with ignoreSystemDescribe counts as one)",
loginFunctionMissing: "You didn't specify a login function",
noTokenError: "Access denied (no token)",
serverResponse: "Server responded",
wrongGreetings: "Wrong value of greetings parameter",
notWhileLogin: "You can't call `%s' function while in login",
loginIsNotAFunction: "Authenticate must be a function",
canExitError: "You can't exit from main interpreter",
invalidCompletion: "Invalid completion",
invalidSelector: 'Sorry, but terminal said that "%s" is not valid '+
'selector!',
invalidTerminalId: 'Invalid Terminal ID',
login: "login",
password: "password",
recursiveCall: 'Recursive call detected, skip'
}
};
// -------------------------------------------------------------------------
// :: All terminal globals
// -------------------------------------------------------------------------
var requests = []; // for canceling on CTRL+D
var terminals = window.terminals = new Cycle(); // list of terminals global in this scope
// state for all terminals, terminals can't have own array fo state because
// there is only one popstate event
var save_state = []; // hold objects returned by export_view by history API
var hash_commands;
var change_hash = false; // don't change hash on Init
var fire_hash_change = true;
var first_instance = true; // used by history state
$.fn.terminal = function(init_interpreter, options) {
// ---------------------------------------------------------------------
// :: helper function
// ---------------------------------------------------------------------
function get_processed_command(command) {
if ($.isFunction(settings.processArguments)) {
return process_command(command, settings.processArguments);
} else if (settings.processArguments) {
return $.terminal.parse_command(command);
} else {
return $.terminal.split_command(command);
}
}
// ---------------------------------------------------------------------
// :: Display object on terminal
// ---------------------------------------------------------------------
function display_object(object) {
if (typeof object === 'string') {
self.echo(object);
} else if (object instanceof Array) {
self.echo($.map(object, function(object) {
return $.json_stringify(object);
}).join(' '));
} else if (typeof object === 'object') {
self.echo($.json_stringify(object));
} else {
self.echo(object);
}
}
// Display line code in the file if line numbers are present
function print_line(url_spec) {
var re = /(.*):([0-9]+):([0-9]+)$/;
// google chrome have line and column after filename
var m = url_spec.match(re);
if (m) {
// TODO: do we need to call pause/resume or return promise?
self.pause();
$.get(m[1], function(response) {
var prefix = location.href.replace(/[^\/]+$/, '');
var file = m[1].replace(prefix, '');
self.echo('[[b;white;]' + file + ']');
var code = response.split('\n');
var n = +m[2]-1;
self.echo(code.slice(n-2, n+3).map(function(line, i) {
if (i == 2) {
line = '[[;#f00;]' +
$.terminal.escape_brackets(line) + ']';
}
return '[' + (n+i) + ']: ' + line;
}).join('\n')).resume();
}, 'text');
}
}
// ---------------------------------------------------------------------
// :: Helper function
// ---------------------------------------------------------------------
function display_json_rpc_error(error) {
if ($.isFunction(settings.onRPCError)) {
settings.onRPCError.call(self, error);
} else {
self.error('&#91;RPC&#93; ' + error.message);
}
}
// ---------------------------------------------------------------------
// :: Create interpreter function from url string
// ---------------------------------------------------------------------
function make_basic_json_rpc(url, auth) {
var interpreter = function(method, params) {
self.pause();
$.jrpc(url, method, params, function(json) {
if (json.error) {
display_json_rpc_error(json.error);
} else {
if ($.isFunction(settings.processRPCResponse)) {
settings.processRPCResponse.call(self, json.result, self);
} else {
display_object(json.result);
}
}
self.resume();
}, ajax_error);
};
//this is the interpreter function
return function(command, terminal) {
if (command === '') {
return;
}
try {
command = get_processed_command(command);
} catch(e) {
// exception can be thrown on invalid regex
terminal.error(e.toString());
return;
//throw e; // this will show stack in other try..catch
}
if (!auth || command.name === 'help') {
// allows to call help without a token
interpreter(command.name, command.args);
} else {
var token = terminal.token();
if (token) {
interpreter(command.name, [token].concat(command.args));
} else {
//should never happen
terminal.error('&#91;AUTH&#93; ' +
strings.noTokenError);
}
}
};
}
// ---------------------------------------------------------------------
// :: Create interpreter function from Object. If the value is object
// :: it will create nested interpreters
// ---------------------------------------------------------------------
function make_object_interpreter(object, arity, login, fallback) {
// function that maps commands to object methods
// it keeps terminal context
return function(user_command, terminal) {
if (user_command === '') {
return;
}
//command = split_command_line(command);
var command;
try {
command = get_processed_command(user_command);
} catch(e) {
// exception can be thrown on invalid regex
self.error(e.toString());
return;
//throw e; // this will show stack in other try..catch
}
/*
if (login) {
var token = self.token(true);
if (token) {
command.args = [token].concat(command.args);
} else {
terminal.error('&#91;AUTH&#93; ' + strings.noTokenError);
return;
}
}*/
var val = object[command.name];
var type = $.type(val);
if (type === 'function') {
if (arity && val.length !== command.args.length) {
self.error("&#91;Arity&#93; " +
sprintf(strings.wrongArity,
command.name,
val.length,
command.args.length));
} else {
return val.apply(self, command.args);
}
} else if (type === 'object' || type === 'string') {
var commands = [];
if (type === 'object') {
commands = Object.keys(val);
val = make_object_interpreter(val,
arity,
login);
}
terminal.push(val, {
prompt: command.name + '> ',
name: command.name,
completion: type === 'object' ? commands : undefined
});
} else {
if ($.isFunction(fallback)) {
fallback(user_command, self);
} else if ($.isFunction(settings.onCommandNotFound)) {
settings.onCommandNotFound(user_command, self);
} else {
terminal.error(sprintf(strings.commandNotFound,
command.name));
}
}
};
}
// ---------------------------------------------------------------------
function ajax_error(xhr, status, error) {
self.resume(); // onAjaxError can use pause/resume call it first
if ($.isFunction(settings.onAjaxError)) {
settings.onAjaxError.call(self, xhr, status, error);
} else if (status !== 'abort') {
self.error('&#91;AJAX&#93; ' + status + ' - ' +
strings.serverResponse + ': \n' +
$.terminal.escape_brackets(xhr.responseText));
}
}
// ---------------------------------------------------------------------
function make_json_rpc_object(url, auth, success) {
$.jrpc(url, 'system.describe', [], function(ret) {
var commands = [];
if (ret.procs) {
var interpreter_object = {};
$.each(ret.procs, function(_, proc) {
interpreter_object[proc.name] = function() {
var append = auth && proc.name != 'help';
var args = Array.prototype.slice.call(arguments);
var args_len = args.length + (append ? 1 : 0);
if (settings.checkArity && proc.params &&
proc.params.length !== args_len) {
self.error("&#91;Arity&#93; " +
sprintf(strings.wrongArity,
proc.name,
proc.params.length,
args_len));
} else {
self.pause();
if (append) {
var token = self.token(true);
if (token) {
args = [token].concat(args);
} else {
self.error('&#91;AUTH&#93; ' +
strings.noTokenError);
}
}
$.jrpc(url, proc.name, args, function(json) {
if (json.error) {
display_json_rpc_error(json.error);
} else {
display_object(json.result);
}
self.resume();
}, ajax_error);
}
};
});
success(interpreter_object);
} else {
success(null);
}
}, function() {
success(null);
});
}
// ---------------------------------------------------------------------
function make_interpreter(user_intrp, login, finalize) {
finalize = finalize || $.noop;
var type = $.type(user_intrp);
var result = {};
var rpc_count = 0; // only one rpc can be use for array
var fn_interpreter;
if (type === 'array') {
var object = {};
// recur will be called when previous acync call is finished
(function recur(interpreters, success) {
if (interpreters.length) {
var first = interpreters[0];
var rest = interpreters.slice(1);
var type = $.type(first);
if (type === 'string') {
rpc_count++;
self.pause();
if (settings.ignoreSystemDescribe) {
if (rpc_count === 1) {
fn_interpreter = make_basic_json_rpc(first, login);
} else {
self.error(strings.oneRPCWithIgnore);
}
recur(rest, success);
} else {
make_json_rpc_object(first, login, function(new_obj) {
// will ignore rpc in array that don't have
// system.describe
if (new_obj) {
$.extend(object, new_obj);
}
self.resume();
recur(rest, success);
});
}
} else if (type === 'function') {
if (fn_interpreter) {
self.error(strings.oneInterpreterFunction);
} else {
fn_interpreter = first;
}
recur(rest, success);
} else if (type === 'object') {
$.extend(object, first);
recur(rest, success);
}
} else {
success();
}
})(user_intrp, function() {
finalize({
interpreter: make_object_interpreter(object,
false,
login,
fn_interpreter),
completion: Object.keys(object)
});
});
} else if (type === 'string') {
if (settings.ignoreSystemDescribe) {
finalize({
interpreter: make_basic_json_rpc(user_intrp, login),
completion: settings.completion
});
} else {
self.pause();
make_json_rpc_object(user_intrp, login, function(object) {
if (object) {
result.interpreter = make_object_interpreter(object,
false,
login);
result.completion = Object.keys(object);
} else {
// no procs in system.describe
result.interpreter = make_basic_json_rpc(user_intrp, login);
result.completion = settings.completion;
}
finalize(result);
self.resume();
});
}
} else if (type === 'object') {
finalize({
interpreter: make_object_interpreter(user_intrp,
settings.checkArity),
completion: Object.keys(user_intrp)
});
} else {
// allow $('<div/>').terminal();
if (type === 'undefined') {
user_intrp = $.noop;
} else if (type !== 'function') {
throw type + " is invalid interpreter value";
}
finalize({
interpreter: user_intrp,
completion: settings.completion
});
}
}
// ---------------------------------------------------------------------
// :: Create JSON-RPC authentication function
// ---------------------------------------------------------------------
function make_json_rpc_login(url, login) {
var method = $.type(login) === 'boolean' ? 'login' : login;
return function(user, passwd, callback, term) {
self.pause();
$.jrpc(url,
method,
[user, passwd],
function(response) {
if (!response.error && response.result) {
callback(response.result);
} else {
// null will trigger message that login fail
callback(null);
}
self.resume();
}, ajax_error);
};
//default name is login so you can pass true
}
// ---------------------------------------------------------------------
// :: Return exception message as string
// ---------------------------------------------------------------------
function exception_message(e) {
if (typeof e === 'string') {
return e;
} else if (typeof e.fileName === 'string') {
return e.fileName + ': ' + e.message;
} else {
return e.message;
}
}
// ---------------------------------------------------------------------
// :: display Exception on terminal
// ---------------------------------------------------------------------
function display_exception(e, label) {
if ($.isFunction(settings.exceptionHandler)) {
settings.exceptionHandler.call(self, e);
} else {
self.exception(e, label);
}
}
// ---------------------------------------------------------------------
function scroll_to_bottom() {
var scrollHeight;
if (scroll_object.prop) {
scrollHeight = scroll_object.prop('scrollHeight');
} else {
scrollHeight = scroll_object.attr('scrollHeight');
}
scroll_object.scrollTop(scrollHeight);
}
// ---------------------------------------------------------------------
// :: validating if object is a string or a function, call that function
// :: and display the exeption if any
// ---------------------------------------------------------------------
function validate(label, object) {
try {
if ($.isFunction(object)) {
object(function() {
// don't care
});
} else if (typeof object !== 'string') {
var msg = label + ' must be string or function';
throw msg;
}
} catch (e) {
display_exception(e, label.toUpperCase());
return false;
}
return true;
}
// ---------------------------------------------------------------------
// :: Draw line - can have line breaks and be longer than the width of
// :: the terminal, there are 2 options raw and finalize
// :: raw - will not encode the string and finalize if a function that
// :: will have div container of the line as first argument
// :: NOTE: it formats and appends lines to output_buffer. The actual
// :: append to terminal output happens in the flush function
// ---------------------------------------------------------------------
var output_buffer = [];
var NEW_LINE = 1;
function buffer_line(string, options) {
// urls should always have formatting to keep url if split
if (settings.convertLinks) {
string = string.replace(email_re, '[[!;;]$1]').
replace(url_nf_re, '[[!;;]$1]');
}
var formatters = $.terminal.defaults.formatters;
var i, len;
if (!options.raw) {
// format using user defined formatters
for (i=0; i<formatters.length; ++i) {
try {
if (typeof formatters[i] == 'function') {
var ret = formatters[i](string);
if (typeof ret == 'string') {
string = ret;
}
}
} catch(e) {
//display_exception(e, 'FORMATTING');
alert('formatting error at formatters[' + i + ']\n' +
(e.stack ? e.stack : e));
}
}
string = $.terminal.encode(string);
}
output_buffer.push(NEW_LINE);
if (!options.raw && (string.length > num_chars ||
string.match(/\n/))) {
var words = options.keepWords;
var array = $.terminal.split_equal(string, num_chars, words);
for (i = 0, len = array.length; i < len; ++i) {
if (array[i] === '' || array[i] === '\r') {
output_buffer.push('<span></span>');
} else {
if (options.raw) {
output_buffer.push(array[i]);
} else {
output_buffer.push($.terminal.format(array[i], {
linksNoReferrer: settings.linksNoReferrer
}));
}
}
}
} else {
if (!options.raw) {
string = $.terminal.format(string, {
linksNoReferrer: settings.linksNoReferrer
});
}
output_buffer.push(string);
}
output_buffer.push(options.finalize);
}
// ---------------------------------------------------------------------
function process_line(line, options) {
// prevent exception in display exception
try {
var line_settings = $.extend({
exec: true,
raw: false,
finalize: $.noop
}, options || {});
var string = $.type(line) === "function" ? line() : line;
string = $.type(string) === "string" ? string : String(string);
if (string !== '') {
if (line_settings.exec) {
string = $.map(string.split(format_exec_re), function(string) {
if (string.match(format_exec_re) &&
!$.terminal.is_formatting(string)) {
// redraw should not execute commands and it have
// and lines variable have all extended commands
string = string.replace(/^\[\[|\]\]$/g, '');
if (prev_command && prev_command.command == string) {
self.error(strings.recursiveCall);
} else {
$.terminal.extended_command(self, string);
}
return '';
} else {
return string;
}
}).join('');
if (string !== '') {
// string can be empty after removing extended commands
buffer_line(string, line_settings);
}
} else {
buffer_line(string, line_settings);
}
}
} catch (e) {
output_buffer = [];
// don't display exception if exception throw in terminal
alert('[Internal Exception(process_line)]:' +
exception_message(e) + '\n' + e.stack);
}
}
// ---------------------------------------------------------------------
// :: Redraw all lines
// ---------------------------------------------------------------------
function redraw() {
command_line.resize(num_chars);
// we don't want reflow while processing lines
var detached_output = output.empty().detach();
var lines_to_show;
if (settings.outputLimit >= 0) {
// flush will limit lines but if there is lot of
// lines we don't need to show them and then remove
// them from terminal
var limit = settings.outputLimit === 0 ?
self.rows() :
settings.outputLimit;
lines_to_show = lines.slice(lines.length-limit-1);
} else {
lines_to_show = lines;
}
try {
output_buffer = [];
$.each(lines_to_show, function(i, line) {
process_line.apply(null, line); // line is an array
});
command_line.before(detached_output); // reinsert output
self.flush();
} catch(e) {
alert('Exception in redraw\n' + e.stack);
}
}
// ---------------------------------------------------------------------
// :: Display user greetings or terminal signature
// ---------------------------------------------------------------------
function show_greetings() {
if (settings.greetings === undefined) {
self.echo(self.signature);
} else if (settings.greetings) {
var type = typeof settings.greetings;
if (type === 'string') {
self.echo(settings.greetings);
} else if (type === 'function') {
settings.greetings.call(self, self.echo);
} else {
self.error(strings.wrongGreetings);
}
}
}
// ---------------------------------------------------------------------
// :: Display prompt and last command
// ---------------------------------------------------------------------
function echo_command(command) {
var prompt = command_line.prompt();
var mask = command_line.mask();
switch (typeof mask) {
case 'string':
command = command.replace(/./g, mask);
break;
case 'boolean':
if (mask) {
command = command.replace(/./g, settings.maskChar);
} else {
command = $.terminal.escape_formatting(command);
}
break;
}
var options = {
finalize: function(div) {
div.addClass('command');
}
};
if ($.isFunction(prompt)) {
prompt(function(string) {
self.echo(string + command, options);
});
} else {
self.echo(prompt + command, options);
}
}
// ---------------------------------------------------------------------
// :: Helper function that restore state. Call import_view or exec
// ---------------------------------------------------------------------
function restore_state(spec) {
// spec [terminal_id, state_index, command]
var terminal = terminals.get()[spec[0]];
if (!terminal) {
throw new Error(strings.invalidTerminalId);
}
var command_idx = spec[1];
if (save_state[command_idx]) { // state exists
terminal.import_view(save_state[command_idx]);
} else {
// restore state
change_hash = false;
var command = spec[2];
if (command) {
terminal.exec(command).then(function() {
change_hash = true;
save_state[command_idx] = terminal.export_view();
});
}
}
/*if (spec[3].length) {
restore_state(spec[3]);
}*/
}
// ---------------------------------------------------------------------
// :: Helper function
// ---------------------------------------------------------------------
function maybe_update_hash() {
if (change_hash) {
fire_hash_change = false;
location.hash = $.json_stringify(hash_commands);
setTimeout(function() {
fire_hash_change = true;
}, 100);
}
}
// ---------------------------------------------------------------------
// :: Wrapper over interpreter, it implements exit and catches all
// :: exeptions from user code and displays them on the terminal
// ---------------------------------------------------------------------
var first_command = true;
var last_command;
var resume_callbacks = [];
var resume_event_bound = false;
function commands(command, silent, exec) {
last_command = command; // for debug
// first command store state of the terminal before the command get
// executed
if (first_command) {
first_command = false;
// execHash need first empty command too
if (settings.historyState || (settings.execHash && exec)) {
if (!save_state.length) {
// first command in first terminal don't have hash
self.save_state();
} else {
self.save_state(null);
}
}
}
function after_exec() {
// variables defined later in commands
if (!exec) {
change_hash = true;
if (settings.historyState) {
self.save_state(command, false);
}
change_hash = saved_change_hash;
}
deferred.resolve();
if ($.isFunction(settings.onAfterCommand)) {
settings.onAfterCommand(self, command);
}
}
try {
// this callback can disable commands
if ($.isFunction(settings.onBeforeCommand)) {
if (settings.onBeforeCommand(self, command) === false) {
return;
}
}
if (!exec) {
prev_command = $.terminal.split_command(command);
}
if (!ghost()) {
// exec execute this function wihout the help of cmd plugin
// that add command to history on enter
if (exec && $.isFunction(settings.historyFilter) &&
settings.historyFilter(command) ||
command.match(settings.historyFilter)) {
command_line.history().append(command);
}
}
var interpreter = interpreters.top();
if (!silent) {
echo_command(command);
}
// new promise will be returned to exec that will resolve his
// returned promise
var deferred = new $.Deferred();
// we need to save sate before commands is deleyd because
// execute_extended_command disable it and it can be executed
// after delay
var saved_change_hash = change_hash;
if (command.match(/^\s*login\s*$/) && self.token(true)) {
if (self.level() > 1) {
self.logout(true);
} else {
self.logout();
}
after_exec();
} else if (settings.exit && command.match(/^\s*exit\s*$/) &&
!in_login) {
var level = self.level();
if (level == 1 && self.get_token() || level > 1) {
if (self.get_token(true)) {
self.set_token(undefined, true);
}
self.pop();
}
after_exec();
} else if (settings.clear && command.match(/^\s*clear\s*$/) &&
!in_login) {
self.clear();
after_exec();
} else {
var position = lines.length-1;
// Call user interpreter function
var result = interpreter.interpreter.call(self, command, self);
if (result !== undefined) {
// auto pause/resume when user return promises
self.pause();
return $.when(result).then(function(result) {
// don't echo result if user echo something
if (result && position === lines.length-1) {
display_object(result);
}
after_exec();
self.resume();
});
} else if (paused) {
var old_command = command;
resume_callbacks.push(function() {
// exec with resume/pause in user code
after_exec();
});
} else {
after_exec();
}
}
return deferred.promise();
} catch (e) {
display_exception(e, 'USER');
self.resume();
throw e;
}
}
// ---------------------------------------------------------------------
// :: The logout function removes Storage, disables history and runs
// :: the login function. This function is called only when options.login
// :: function is defined. The check for this is in the self.pop method
// ---------------------------------------------------------------------
function global_logout() {
if ($.isFunction(settings.onBeforeLogout)) {
try {
if (settings.onBeforeLogout(self) === false) {
return;
}
} catch (e) {
display_exception(e, 'onBeforeLogout');
throw e;
}
}
clear_loging_storage();
if ($.isFunction(settings.onAfterLogout)) {
try {
settings.onAfterLogout(self);
} catch (e) {
display_exception(e, 'onAfterlogout');
throw e;
}
}
self.login(settings.login, true, initialize);
}
// ---------------------------------------------------------------------
function clear_loging_storage() {
var name = self.prefix_name(true) + '_';
$.Storage.remove(name + 'token');
$.Storage.remove(name + 'login');
}
// ---------------------------------------------------------------------
// :: Save the interpreter name for use with purge
// ---------------------------------------------------------------------
function maybe_append_name(interpreter_name) {
var storage_key = self.prefix_name() + '_interpreters';
var names = $.Storage.get(storage_key);
if (names) {
names = $.parseJSON(names);
} else {
names = [];
}
if ($.inArray(interpreter_name, names) == -1) {
names.push(interpreter_name);
$.Storage.set(storage_key, $.json_stringify(names));
}
}
// ---------------------------------------------------------------------
// :: Function enables history, sets prompt, runs interpreter function
// ---------------------------------------------------------------------
function prepare_top_interpreter(silent) {
var interpreter = interpreters.top();
var name = self.prefix_name(true);
if (!ghost()) {
maybe_append_name(name);
}
command_line.name(name);
if ($.isFunction(interpreter.prompt)) {
command_line.prompt(function(command) {
interpreter.prompt(command, self);
});
} else {
command_line.prompt(interpreter.prompt);
}
command_line.set('');
if (!silent && $.isFunction(interpreter.onStart)) {
interpreter.onStart(self);
}
}
// ---------------------------------------------------------------------
var local_first_instance;
function initialize() {
prepare_top_interpreter();
show_greetings();
// was_paused flag is workaround for case when user call exec before
// login and pause in onInit, 3rd exec will have proper timing (will
// execute after onInit resume)
var was_paused = false;
if ($.isFunction(settings.onInit)) {
onPause = function() { // local in terminal
was_paused = true;
};
try {
settings.onInit(self);
} catch (e) {
display_exception(e, 'OnInit');
// throw e; // it will be catched by terminal
} finally {
onPause = $.noop;
if (!was_paused) {
// resume login if user didn't call pause in onInit
// if user pause in onInit wait with exec until it
// resume
self.resume();
}
}
}
function hashchange() {
if (fire_hash_change && settings.execHash) {
try {
if (location.hash) {
var hash = location.hash.replace(/^#/, '');
hash_commands = $.parseJSON(decodeURIComponent(hash));
} else {
hash_commands = [];
}
if (hash_commands.length) {
restore_state(hash_commands[hash_commands.length-1]);
} else if (save_state[0]) {
self.import_view(save_state[0]);
}
} catch(e) {
display_exception(e, 'TERMINAL');
}
}
}
if (first_instance) {
first_instance = false;
if ($.fn.hashchange) {
$(window).hashchange(hashchange);
} else {
$(window).bind('hashchange', hashchange);
}
}
}
// ---------------------------------------------------------------------
// :: function complete the command
// ---------------------------------------------------------------------
function complete_helper(command, string, commands) {
if (settings.clear && $.inArray('clear', commands) == -1) {
commands.push('clear');
}
if (settings.exit && $.inArray('exit', commands) == -1) {
commands.push('exit');
}
var test = command_line.get().substring(0, command_line.position());
if (test !== command) {
// command line changed between TABS - ignore
return;
}
var regex = new RegExp('^' + $.terminal.escape_regex(string));
var matched = [];
for (var i=commands.length; i--;) {
if (regex.test(commands[i])) {
matched.push(commands[i]);
}
}
if (matched.length === 1) {
self.insert(matched[0].replace(regex, ''));
} else if (matched.length > 1) {
if (tab_count >= 2) {
echo_command(command);
var text = matched.reverse().join('\t');
self.echo($.terminal.escape_brackets(text), {keepWords: true});
tab_count = 0;
} else {
var found = false;
var found_index;
var j;
loop:
for (j=string.length; j<matched[0].length; ++j) {
for (i=1; i<matched.length; ++i) {
if (matched[0].charAt(j) !== matched[i].charAt(j)) {
break loop;
}
}
found = true;
}
if (found) {
self.insert(matched[0].slice(0, j).replace(regex, ''));
}
}
}
}
// ---------------------------------------------------------------------
// :: If Ghost don't store anything in localstorage
// ---------------------------------------------------------------------
function ghost() {
return in_login || command_line.mask() !== false;
}
// ---------------------------------------------------------------------
// :: Keydown event handler
// ---------------------------------------------------------------------
function key_down(e) {
// Prevent to be executed by cmd: CTRL+D, TAB, CTRL+TAB (if more
// then one terminal)
var result, i, top = interpreters.top();
if (!self.paused() && self.enabled()) {
if ($.isFunction(top.keydown)) {
result = top.keydown(e, self);
if (result !== undefined) {
return result;
}
} else if ($.isFunction(settings.keydown)) {
result = settings.keydown(e, self);
if (result !== undefined) {
return result;
}
}
var completion;
if ((settings.completion &&
$.type(settings.completion) != 'boolean') &&
top.completion === undefined) {
completion = settings.completion;
} else {
completion = top.completion;
}
// after text pasted into textarea in cmd plugin
self.oneTime(10, function() {
on_scrollbar_show_resize();
});
if (e.which !== 9) { // not a TAB
tab_count = 0;
}
if (e.which === 68 && e.ctrlKey) { // CTRL+D
if (!in_login) {
if (command_line.get() === '') {
if (interpreters.size() > 1 ||
settings.login !== undefined) {
self.pop('');
} else {
self.resume();
self.echo('');
}
} else {
self.set_command('');
}
}
return false;
} else if (e.which === 76 && e.ctrlKey) { // CTRL+L
self.clear();
} else if (completion && e.which === 9) { // TAB
// TODO: move this to cmd plugin
// add completion = array | function
// !!! Problem complete more then one key need terminal
++tab_count;
// cursor can be in the middle of the command
// so we need to get the text before the cursor
var pos = command_line.position();
var command = command_line.get().substring(0, pos);
var strings = command.split(' ');
var string; // string before cursor that will be completed
if (strings.length == 1) {
string = strings[0];
} else {
string = strings[strings.length-1];
for (i=strings.length-1; i>0; i--) {
// treat escape space as part of the string
if (strings[i-1][strings[i-1].length-1] == '\\') {
string = strings[i-1] + ' ' + string;
} else {
break;
}
}
}
switch ($.type(completion)) {
case 'function':
completion(self, string, function(commands) {
complete_helper(command, string, commands);
});
break;
case 'array':
complete_helper(command, string, completion);
break;
default:
// terminal will not catch this because it's an event
throw new Error(strings.invalidCompletion);
}
return false;
} else if (e.which === 86 && e.ctrlKey) { // CTRL+V
self.oneTime(1, function() {
scroll_to_bottom();
});
return;
} else if (e.which === 9 && e.ctrlKey) { // CTRL+TAB
if (terminals.length() > 1) {
self.focus(false);
return false;
}
} else if (e.which === 34) { // PAGE DOWN
self.scroll(self.height());
} else if (e.which === 33) { // PAGE UP
self.scroll(-self.height());
} else {
self.attr({scrollTop: self.attr('scrollHeight')});
}
} else if (e.which === 68 && e.ctrlKey) { // CTRL+D (if paused)
if (requests.length) {
for (i=requests.length; i--;) {
var r = requests[i];
if (4 !== r.readyState) {
try {
r.abort();
} catch (error) {
self.error(strings.ajaxAbortError);
}
}
}
requests = [];
// only resume if there are ajax calls
self.resume();
}
return false;
}
}
// ---------------------------------------------------------------------
var self = this;
if (this.length > 1) {
return this.each(function() {
$.fn.terminal.call($(this),
init_interpreter,
$.extend({name: self.selector}, options));
});
}
// terminal already exists
if (self.data('terminal')) {
return self.data('terminal');
}
if (self.length === 0) {
throw sprintf($.terminal.defaults.strings.invalidSelector, self.selector);
}
//var names = []; // stack if interpeter names
var scroll_object;
var prev_command; // used for name on the terminal if not defined
var loged_in = false;
var tab_count = 0; // for tab completion
// array of line objects:
// - function (called whenever necessary, result is printed)
// - array (expected form: [line, settings])
// - anything else (cast to string when painted)
var lines = [];
var output; // .terminal-output jquery object
var terminal_id = terminals.length();
var num_chars; // numer of chars in line
var num_rows; // number of lines that fit without scrollbar
var command_list = []; // for tab completion
var url;
var logins = new Stack(); // stack of logins
var in_login = false;//some Methods should not be called when login
// TODO: Try to use mutex like counter for pause/resume
var onPause = $.noop;//used to indicate that user call pause onInit
var old_width, old_height;
var delayed_commands = []; // used when exec commands while paused
var settings = $.extend({},
$.terminal.defaults,
{name: self.selector},
options || {});
var strings = $.terminal.defaults.strings;
var enabled = settings.enabled, frozen;
var paused = false;
// -----------------------------------------------------------------
// TERMINAL METHODS
// -----------------------------------------------------------------
$.extend(self, $.omap({
id: function() {
return terminal_id;
},
// -------------------------------------------------------------
// :: Clear the output
// -------------------------------------------------------------
clear: function() {
output.html('');
lines = [];
try {
settings.onClear(self);
} catch (e) {
display_exception(e, 'onClear');
throw e;
}
self.attr({ scrollTop: 0});
return self;
},
// -------------------------------------------------------------
// :: Return an object that can be used with import_view to
// :: restore the state
// -------------------------------------------------------------
export_view: function() {
return $.extend({}, {
focus: enabled,
mask: command_line.mask(),
prompt: self.get_prompt(),
command: self.get_command(),
position: command_line.position(),
lines: clone(lines),
interpreters: interpreters.clone()
}, $.isFunction(settings.onExport) ? settings.onExport() : {});
},
// -------------------------------------------------------------
// :: Restore the state of the previous exported view
// -------------------------------------------------------------
import_view: function(view) {
if (in_login) {
throw new Error(sprintf(strings.notWhileLogin, 'import_view'));
}
if ($.isFunction(settings.onImport)) {
settings.onImport(view);
}
self.set_prompt(view.prompt);
self.set_command(view.command);
command_line.position(view.position);
command_line.mask(view.mask);
if (view.focus) {
self.focus();
}
lines = clone(view.lines);
interpreters = view.interpreters;
redraw();
return self;
},
// -------------------------------------------------------------
// :: Store current terminal state
// -------------------------------------------------------------
save_state: function(command, ignore_hash, index) {
//save_state.push({view:self.export_view(), join:[]});
if (typeof index != 'undefined') {
save_state[index] = self.export_view();
} else {
save_state.push(self.export_view());
}
if (!$.isArray(hash_commands)) {
hash_commands = [];
}
if (command !== undefined && !ignore_hash) {
var state = [
terminal_id,
save_state.length-1,
command
];
hash_commands.push(state);
maybe_update_hash();
}
},
// -------------------------------------------------------------
// :: Execute a command, it will handle commands that do AJAX
// :: calls and have delays, if the second argument is set to
// :: true it will not echo executed command
// -------------------------------------------------------------
exec: function(command, silent, deferred) {
if ($.isArray(command)) {
return $.when.apply($, $.map(command, function(command) {
return self.exec(command, silent);
}));
}
// both commands executed here (resume will call Term::exec)
var d = deferred || new $.Deferred();
if (paused) {
// delay command multiple time
delayed_commands.push([command, silent, d]);
} else {
// commands may return promise from user code
// it will resolve exec promise when user promise
// is resolved
commands(command, silent, true).then(function() {
d.resolve(self);
});
}
return d.promise();
},
// -------------------------------------------------------------
// :: bypass login function that wait until you type user/pass
// :: it hide implementation detail
// -------------------------------------------------------------
autologin: function(user, token, silent) {
self.trigger('terminal.autologin', [user, token, silent]);
return self;
},
// -------------------------------------------------------------
// :: Function changes the prompt of the command line to login
// :: with a password and calls the user login function with
// :: the callback that expects a token. The login is successful
// :: if the user calls it with value that is truthy
// -------------------------------------------------------------
login: function(auth, infinite, success, error) {
logins.push([].slice.call(arguments));
if (in_login) {
throw new Error(sprintf(strings.notWhileLogin, 'login'));
}
if (!$.isFunction(auth)) {
throw new Error(strings.loginIsNotAFunction);
}
if (self.token(true) && self.login_name(true)) {
if ($.isFunction(success)) {
success();
}
return self;
}
var user = null;
// don't store login data in history
if (settings.history) {
command_line.history().disable();
}
// so we know how many times call pop
var level = self.level();
in_login = true;
function login_callback(user, token, silent, event) {
if (token) {
while (self.level() > level) {
self.pop();
}
if (settings.history) {
command_line.history().enable();
}
var name = self.prefix_name(true) + '_';
$.Storage.set(name + 'token', token);
$.Storage.set(name + 'login', user);
in_login = false;
if ($.isFunction(success)) {
// will be used internaly since users know
// when login success (they decide when
// it happen by calling the callback -
// this funtion)
success();
}
} else {
if (infinite) {
if (!silent) {
self.error(strings.wrongPasswordTryAgain);
}
self.pop().set_mask(false);
} else {
in_login = false;
if (!silent) {
self.error(strings.wrongPassword);
}
self.pop().pop();
}
// used only to call pop in push
if ($.isFunction(error)) {
error();
}
}
self.off('terminal.autologin');
}
self.on('terminal.autologin', function(event, user, token, silent) {
login_callback(user, token, silent);
});
self.push(function(user) {
self.set_mask(settings.maskChar).push(function(pass) {
try {
auth.call(self, user, pass, function(token, silent) {
login_callback(user, token, silent);
});
} catch(e) {
display_exception(e, 'AUTH');
}
}, {
prompt: strings.password + ': ',
name: 'password'
});
}, {
prompt: strings.login + ': ',
name: 'login'
});
return self;
},
// -------------------------------------------------------------
// :: User defined settings and defaults as well
// -------------------------------------------------------------
settings: function() {
return settings;
},
// -------------------------------------------------------------
// :: Return commands function from top interpreter
// -------------------------------------------------------------
commands: function() {
return interpreters.top().interpreter;
},
// -------------------------------------------------------------
// :: Low Level method that overwrites interpreter
// -------------------------------------------------------------
setInterpreter: function() {
if (window.console && console.warn) {
console.warn('This function is deprecated, use set_inte'+
'rpreter insead!');
}
self.set_interpreter.apply(self, arguments);
},
// -------------------------------------------------------------
set_interpreter: function(user_intrp, login) {
function overwrite_interpreter() {
self.pause();
make_interpreter(user_intrp, !!login, function(result) {
self.resume();
var top = interpreters.top();
$.extend(top, result);
prepare_top_interpreter(true);
});
}
if ($.type(user_intrp) == 'string' && login) {
self.login(make_json_rpc_login(user_intrp, login),
true,
overwrite_interpreter);
} else {
overwrite_interpreter();
}
},
// -------------------------------------------------------------
// :: Show user greetings or terminal signature
// -------------------------------------------------------------
greetings: function() {
show_greetings();
return self;
},
// -------------------------------------------------------------
// :: Return true if terminal is paused false otherwise
// -------------------------------------------------------------
paused: function() {
return paused;
},
// -------------------------------------------------------------
// :: Pause the terminal, it should be used for ajax calls
// -------------------------------------------------------------
pause: function() {
onPause();
if (!paused && command_line) {
paused = true;
command_line.disable().hidden();
if ($.isFunction(settings.onPause)) {
settings.onPause();
}
}
return self;
},
// -------------------------------------------------------------
// :: Resume the previously paused terminal
// -------------------------------------------------------------
resume: function() {
if (paused && command_line) {
paused = false;
command_line.enable().visible();
var original = delayed_commands;
delayed_commands = [];
(function recur() {
if (original.length) {
self.exec.apply(self, original.shift()).then(recur);
} else {
self.trigger('resume');
var fn = resume_callbacks.shift();
if (fn) {
fn();
}
scroll_to_bottom();
if ($.isFunction(settings.onResume)) {
settings.onResume();
}
}
})();
}
return self;
},
// -------------------------------------------------------------
// :: Return the number of characters that fit into the width of
// :: the terminal
// -------------------------------------------------------------
cols: function() {
return settings.numChars?settings.numChars:get_num_chars(self);
},
// -------------------------------------------------------------
// :: Return the number of lines that fit into the height of the
// :: terminal
// -------------------------------------------------------------
rows: function() {
return settings.numRows?settings.numRows:get_num_rows(self);
},
// -------------------------------------------------------------
// :: Return the History object
// -------------------------------------------------------------
history: function() {
return command_line.history();
},
// -------------------------------------------------------------
// :: toggle recording of history state
// -------------------------------------------------------------
history_state: function(toggle) {
if (toggle) {
// if set to true and if set from user command we need
// not to include the command
self.oneTime(1, function() {
settings.historyState = true;
if (!save_state.length) {
self.save_state();
} else if (terminals.length() > 1) {
self.save_state(null);
}
});
} else {
settings.historyState = false;
}
return self;
},
// -------------------------------------------------------------
// :: Switch to the next terminal
// -------------------------------------------------------------
next: function() {
if (terminals.length() === 1) {
return self;
} else {
var offsetTop = self.offset().top;
var height = self.height();
var scrollTop = self.scrollTop();
if (!is_scrolled_into_view(self)) {
self.enable();
$('html,body').animate({
scrollTop: offsetTop-50
}, 500);
return self;
} else {
terminals.front().disable();
var next = terminals.rotate().enable();
// 100 provides buffer in viewport
var x = next.offset().top - 50;
$('html,body').animate({scrollTop: x}, 500);
try {
settings.onTerminalChange(next);
} catch (e) {
display_exception(e, 'onTerminalChange');
throw e;
}
return next;
}
}
},
// -------------------------------------------------------------
// :: Make the terminal in focus or blur depending on the first
// :: argument. If there is more then one terminal it will
// :: switch to next one, if the second argument is set to true
// :: the events will be not fired. Used on init
// -------------------------------------------------------------
focus: function(toggle, silent) {
if (terminals.length() === 1) {
if (toggle === false) {
try {
if (!silent && settings.onBlur(self) !== false ||
silent) {
self.disable();
}
} catch (e) {
display_exception(e, 'onBlur');
throw e;
}
} else {
try {
if (!silent && settings.onFocus(self) !== false ||
silent) {
self.enable();
}
} catch (e) {
display_exception(e, 'onFocus');
throw e;
}
}
} else {
if (toggle === false) {
self.next();
} else {
var front = terminals.front();
if (front != self) {
front.disable();
if (!silent) {
try {
settings.onTerminalChange(self);
} catch (e) {
display_exception(e, 'onTerminalChange');
throw e;
}
}
}
terminals.set(self);
self.enable();
}
}
// why this delay - it can't be use for mobile
/*
self.oneTime(1, function() {
});
*/
return self;
},
// -------------------------------------------------------------
// :: Disable/Enable terminal that can be enabled by click
// -------------------------------------------------------------
freeze: function(freeze) {
if (freeze) {
self.disable();
frozen = true;
} else {
frozen = false;
self.enable();
}
},
// -------------------------------------------------------------
// :: check if terminal is frozen
// -------------------------------------------------------------
frozen: function() {
return frozen;
},
// -------------------------------------------------------------
// :: Enable the terminal
// -------------------------------------------------------------
enable: function() {
if (!enabled && !frozen) {
if (num_chars === undefined) {
//enabling first time
self.resize();
}
if (command_line) {
command_line.enable();
enabled = true;
}
}
return self;
},
// -------------------------------------------------------------
// :: Disable the terminal
// -------------------------------------------------------------
disable: function() {
if (enabled && command_line && !frozen) {
enabled = false;
command_line.disable();
}
return self;
},
// -------------------------------------------------------------
// :: return true if the terminal is enabled
// -------------------------------------------------------------
enabled: function() {
return enabled;
},
// -------------------------------------------------------------
// :: Return the terminal signature depending on the size of the terminal
// -------------------------------------------------------------
signature: function() {
var cols = self.cols();
var i = cols < 15 ? null : cols < 35 ? 0 : cols < 55 ? 1 : cols < 64 ? 2 : cols < 75 ? 3 : 4;
if (i !== null) {
return signatures[i].join('\n') + '\n';
} else {
return '';
}
},
// -------------------------------------------------------------
// :: Return the version number
// -------------------------------------------------------------
version: function() {
return $.terminal.version;
},
// -------------------------------------------------------------
// :: Return actual command line object (jquery object with cmd
// :: methods)
// -------------------------------------------------------------
cmd: function() {
return command_line;
},
// -------------------------------------------------------------
// :: Return the current command entered by terminal
// -------------------------------------------------------------
get_command: function() {
return command_line.get();
},
// -------------------------------------------------------------
// :: Change the command line to the new one
// -------------------------------------------------------------
set_command: function(command) {
command_line.set(command);
return self;
},
// -------------------------------------------------------------
// :: Insert text into the command line after the cursor
// -------------------------------------------------------------
insert: function(string) {
if (typeof string === 'string') {
command_line.insert(string);
return self;
} else {
throw "insert function argument is not a string";
}
},
// -------------------------------------------------------------
// :: Set the prompt of the command line
// -------------------------------------------------------------
set_prompt: function(prompt) {
if (validate('prompt', prompt)) {
if ($.isFunction(prompt)) {
command_line.prompt(function(callback) {
prompt(callback, self);
});
} else {
command_line.prompt(prompt);
}
interpreters.top().prompt = prompt;
}
return self;
},
// -------------------------------------------------------------
// :: Return the prompt used by the terminal
// -------------------------------------------------------------
get_prompt: function() {
return interpreters.top().prompt;
// command_line.prompt(); - can be a wrapper
//return command_line.prompt();
},
// -------------------------------------------------------------
// :: Enable or Disable mask depedning on the passed argument
// :: the mask can also be character (in fact it will work with
// :: strings longer then one)
// -------------------------------------------------------------
set_mask: function(mask) {
command_line.mask(mask === true ? settings.maskChar : mask);
return self;
},
// -------------------------------------------------------------
// :: Return the ouput of the terminal as text
// -------------------------------------------------------------
get_output: function(raw) {
if (raw) {
return lines;
} else {
return $.map(lines, function(item) {
return $.isFunction(item[0]) ? item[0]() : item[0];
}).join('\n');
}
},
// -------------------------------------------------------------
// :: Recalculate and redraw everything
// -------------------------------------------------------------
resize: function(width, height) {
if (!self.is(':visible')) {
// delay resize if terminal not visible
self.stopTime('resize');
self.oneTime(500, 'resize', function() {
self.resize(width, height);
});
} else {
if (width && height) {
self.width(width);
self.height(height);
}
width = self.width();
height = self.height();
var new_num_chars = self.cols();
var new_num_rows = self.rows();
// only if number of chars changed
if (new_num_chars !== num_chars ||
new_num_rows !== num_rows) {
num_chars = new_num_chars;
num_rows = new_num_rows;
redraw();
var top = interpreters.top();
if ($.isFunction(top.resize)) {
top.resize(self);
} else if ($.isFunction(settings.onResize)) {
settings.onResize(self);
}
old_height = height;
old_width = width;
scroll_to_bottom();
}
}
return self;
},
// -------------------------------------------------------------
// :: Flush the output to the terminal
// -------------------------------------------------------------
flush: function() {
try {
var wrapper;
// print all lines
$.each(output_buffer, function(i, line) {
if (line === NEW_LINE) {
wrapper = $('<div></div>');
} else if ($.isFunction(line)) {
// this is finalize function from echo
wrapper.appendTo(output);
try {
line(wrapper);
/* this don't work with resize
line(wrapper, function(user_finalize) {
// TODO:
//user_finalize need to be save in line object
user_finalize(wrapper);
});*/
} catch (e) {
display_exception(e, 'USER:echo(finalize)');
}
} else {
$('<div/>').html(line).
appendTo(wrapper).width('100%');
}
});
if (settings.outputLimit >= 0) {
var limit = settings.outputLimit === 0 ?
self.rows() :
settings.outputLimit;
var $lines = output.find('div div');
if ($lines.length > limit) {
var max = lines.length-limit+1;
var for_remove = $lines.slice(0, max);
// you can't get parent if you remove the
// element so we first get the parent
var parents = for_remove.parent();
for_remove.remove();
parents.each(function() {
var self = $(this);
if (self.is(':empty')) {
// there can be divs inside parent that
// was not removed
self.remove();
}
});
}
}
scroll_to_bottom();
output_buffer = [];
} catch (e) {
alert('[Flush] ' + exception_message(e) + '\n' +
e.stack);
}
return self;
},
// -------------------------------------------------------------
// :: Update the output line - line number can be negative
// -------------------------------------------------------------
update: function(line, string) {
if (line < 0) {
line = lines.length + line; // yes +
}
if (!lines[line]) {
self.error('Invalid line number ' + line);
} else {
lines[line][0] = string;
// it would be hard to figure out which div need to be
// updated so we update everything
redraw();
}
return self;
},
// -------------------------------------------------------------
// :: Print data to the terminal output. It can have two options
// :: a function that is called with the container div that
// :: holds the output (as a jquery object) every time the
// :: output is printed (including resize and scrolling)
// :: If the line is a function it will be called for every
// :: redraw.
// :: it use $.when so you can echo a promise
// -------------------------------------------------------------
echo: function(string, options) {
string = string || '';
$.when(string).then(function(string) {
try {
output_buffer = [];
var locals = $.extend({
flush: true,
raw: settings.raw,
finalize: $.noop,
keepWords: false
}, options || {});
process_line(string, locals);
// extended commands should be processed only once
// in echo and not on redraw
lines.push([string, $.extend(locals, {
exec: false
})]);
if (locals.flush) {
self.flush();
}
num_rows = get_num_rows(self);
on_scrollbar_show_resize();
} catch (e) {
// if echo throw exception we can't use error to
// display that exception
alert('[Terminal.echo] ' + exception_message(e) +
'\n' + e.stack);
}
});
return self;
},
// -------------------------------------------------------------
// :: echo red text
// -------------------------------------------------------------
error: function(message, finalize) {
//quick hack to fix trailing backslash
var str = $.terminal.escape_brackets(message).
replace(/\\$/, '&#92;').
replace(url_re, ']$1[[;;;error]');
return self.echo('[[;;;error]' + str + ']', finalize);
},
// -------------------------------------------------------------
// :: Display Exception on terminal
// -------------------------------------------------------------
exception: function(e, label) {
var message = exception_message(e);
if (label) {
message = '&#91;' + label + '&#93;: ' + message;
}
if (message) {
self.error(message, {
finalize: function(div) {
div.addClass('exception message');
}
});
}
if (typeof e.fileName === 'string') {
//display filename and line which throw exeption
self.pause();
$.get(e.fileName, function(file) {
self.resume();
var num = e.lineNumber - 1;
var line = file.split('\n')[num];
if (line) {
self.error('[' + e.lineNumber + ']: ' + line);
}
});
}
if (e.stack) {
self.echo(e.stack.split(/\n/g).map(function(trace) {
return '[[;;;error]' + trace.replace(url_re, function(url) {
return ']' + url + '[[;;;error]';
}) + ']';
}).join('\n'), {
finalize: function(div) {
div.addClass('exception stack-trace');
}
});
}
},
// -------------------------------------------------------------
// :: Scroll Div that holds the terminal
// -------------------------------------------------------------
scroll: function(amount) {
var pos;
amount = Math.round(amount);
if (scroll_object.prop) { // work with jQuery > 1.6
if (amount > scroll_object.prop('scrollTop') && amount > 0) {
scroll_object.prop('scrollTop', 0);
}
pos = scroll_object.prop('scrollTop');
scroll_object.scrollTop(pos + amount);
} else {
if (amount > scroll_object.attr('scrollTop') && amount > 0) {
scroll_object.attr('scrollTop', 0);
}
pos = scroll_object.attr('scrollTop');
scroll_object.scrollTop(pos + amount);
}
return self;
},
// -------------------------------------------------------------
// :: Exit all interpreters and logout. The function will throw
// :: exception if there is no login provided
// -------------------------------------------------------------
logout: function(local) {
if (in_login) {
throw new Error(sprintf(strings.notWhileLogin, 'import_view'));
}
if (local) {
var login = logins.pop();
self.set_token(undefined, true);
self.login.apply(self, login);
} else {
while (interpreters.size() > 0) {
// pop will call global_logout that will call login
// and size will be > 0; this is workaround the problem
if (self.pop()) {
break;
}
}
}
return self;
},
// -------------------------------------------------------------
// :: Function returns the token returned by callback function
// :: in login function. It does nothing (return undefined) if
// :: there is no login
// -------------------------------------------------------------
token: function(local) {
return $.Storage.get(self.prefix_name(local) + '_token');
},
// -------------------------------------------------------------
// :: Function sets the token to the supplied value. This function
// :: works regardless of wherer settings.login is supplied
// -------------------------------------------------------------
set_token: function(token, local) {
var name = self.prefix_name(local) + '_token';
if (typeof token == 'undefined') {
$.Storage.remove(name, token);
} else {
$.Storage.set(name, token);
}
return self;
},
// -------------------------------------------------------------
// :: Function get the token either set by the login method or
// :: by the set_token method.
// -------------------------------------------------------------
get_token: function(local) {
return $.Storage.get(self.prefix_name(local) + '_token');
},
// -------------------------------------------------------------
// :: Function return Login name entered by the user
// -------------------------------------------------------------
login_name: function(local) {
return $.Storage.get(self.prefix_name(local) + '_login');
},
// -------------------------------------------------------------
// :: Function returns the name of current interpreter
// -------------------------------------------------------------
name: function() {
return interpreters.top().name;
},
// -------------------------------------------------------------
// :: Function return prefix name for login/token
// -------------------------------------------------------------
prefix_name: function(local) {
var name = (settings.name ? settings.name + '_': '') +
terminal_id;
if (local && interpreters.size() > 1) {
var local_name = interpreters.map(function(intrp) {
return intrp.name;
}).slice(1).join('_');
if (local_name) {
name += '_' + local_name;
}
}
return name;
},
// -------------------------------------------------------------
// :: wrapper for common use case
// -------------------------------------------------------------
read: function(message, callback) {
var d = new $.Deferred();
self.push(function(text) {
self.pop();
if ($.isFunction(callback)) {
callback(text);
}
d.resolve(text);
}, {
prompt: message
});
return d.promise();
},
// -------------------------------------------------------------
// :: Push a new interenter on the Stack
// -------------------------------------------------------------
push: function(interpreter, options) {
options = options || {};
if (!options.name && prev_command) {
// push is called in login
options.name = prev_command.name;
}
if (options.prompt === undefined) {
options.prompt = options.name + ' ';
}
//names.push(options.name);
var top = interpreters.top();
if (top) {
top.mask = command_line.mask();
}
var was_paused = paused;
self.pause();
make_interpreter(interpreter, !!options.login, function(ret) {
// result is object with interpreter and completion
// properties
interpreters.push($.extend({}, ret, options));
if (options.login) {
var type = $.type(options.login);
if (type == 'function') {
// self.pop on error
self.login(options.login,
false,
prepare_top_interpreter,
self.pop);
} else if ($.type(interpreter) == 'string' &&
type == 'string' || type == 'boolean') {
self.login(make_json_rpc_login(interpreter,
options.login),
false,
prepare_top_interpreter,
self.pop);
}
} else {
prepare_top_interpreter();
}
if (!was_paused) {
self.resume();
}
});
return self;
},
// -------------------------------------------------------------
// :: Remove the last interpreter from the Stack
// -------------------------------------------------------------
pop: function(string) {
if (string !== undefined) {
echo_command(string);
}
var token = self.token(true);
if (interpreters.size() == 1) {
if (settings.login) {
global_logout();
if ($.isFunction(settings.onExit)) {
try {
settings.onExit(self);
} catch (e) {
display_exception(e, 'onExit');
throw e;
}
}
return true;
} else {
self.error(strings.canExitError);
}
} else {
if (self.token(true)) {
clear_loging_storage();
}
var current = interpreters.pop();
prepare_top_interpreter();
if ($.isFunction(current.onExit)) {
try {
current.onExit(self);
} catch (e) {
display_exception(e, 'onExit');
throw e;
}
}
// restore mask
self.set_mask(interpreters.top().mask);
}
return self;
},
// -------------------------------------------------------------
// :: Change terminal option(s) at runtime
// -------------------------------------------------------------
option: function(object_or_name, value) {
if (typeof value == 'undefined') {
if (typeof object_or_name == 'string') {
return settings[object_or_name];
} else if (typeof object_or_name == 'object') {
$.each(object_or_name, function(key, value) {
settings[key] = value;
});
}
} else {
settings[object_or_name] = value;
}
return self;
},
// -------------------------------------------------------------
// :: Return how deep you are in nested interpreters
// -------------------------------------------------------------
level: function() {
return interpreters.size();
},
// -------------------------------------------------------------
// :: Reinitialize the terminal
// -------------------------------------------------------------
reset: function() {
self.clear();
while(interpreters.size() > 1) {
interpreters.pop();
}
initialize();
return self;
},
// -------------------------------------------------------------
// :: Remove all local storage left by terminal, it will not
// :: logout you until you refresh the browser
// -------------------------------------------------------------
purge: function() {
var prefix = self.prefix_name() + '_';
var names = $.Storage.get(prefix + 'interpreters');
$.each($.parseJSON(names), function(_, name) {
$.Storage.remove(name + '_commands');
$.Storage.remove(name + '_token');
$.Storage.remove(name + '_login');
});
command_line.purge();
$.Storage.remove(prefix + 'interpreters');
return self;
},
// -------------------------------------------------------------
// :: Remove all events and DOM nodes left by terminal, it will
// :: not purge the terminal so you will have the same state
// :: when you refresh the browser
// -------------------------------------------------------------
destroy: function() {
command_line.destroy().remove();
output.remove();
$(document).unbind('.terminal');
$(window).unbind('.terminal');
self.unbind('click mousewheel');
self.removeData('terminal').removeClass('terminal');
if (settings.width) {
self.css('width', '');
}
if (settings.height) {
self.css('height', '');
}
$(window).off('blur', blur_terminal).
off('focus', focus_terminal);
terminals.remove(terminal_id);
return self;
}
}, function(name, fun) {
// wrap all functions and display execptions
return function() {
try {
return fun.apply(self, [].slice.apply(arguments));
} catch (e) {
// exec catch by command (resume call exec)
if (name !== 'exec' && name !== 'resume') {
display_exception(e, 'TERMINAL');
}
throw e;
}
};
}));
// -----------------------------------------------------------------
var on_scrollbar_show_resize = (function() {
var scroll_bars = have_scrollbars(self);
return function() {
if (scroll_bars !== have_scrollbars(self)) {
// if the scollbar appearance changes we will have a
// different number of chars
self.resize();
scroll_bars = have_scrollbars(self);
}
};
})();
// -----------------------------------------------------------------
// INIT CODE
// -----------------------------------------------------------------
if (settings.width) {
self.width(settings.width);
}
if (settings.height) {
self.height(settings.height);
}
var agent = navigator.userAgent.toLowerCase();
if (!agent.match(/(webkit)[ \/]([\w.]+)/) &&
self[0].tagName.toLowerCase() == 'body') {
scroll_object = $('html');
} else {
scroll_object = self;
}
// register ajaxSend for cancel requests on CTRL+D
$(document).bind('ajaxSend.terminal', function(e, xhr, opt) {
requests.push(xhr);
});
output = $('<div>').addClass('terminal-output').appendTo(self);
self.addClass('terminal');
// keep focus in clipboard textarea in mobile
/*
if (('ontouchstart' in window) || window.DocumentTouch &&
document instanceof DocumentTouch) {
self.click(function() {
self.find('textarea').focus();
});
self.find('textarea').focus();
}
*/
/*
self.bind('touchstart.touchScroll', function() {
});
self.bind('touchmove.touchScroll', function() {
});
*/
//$('<input type="text"/>').hide().focus().appendTo(self);
// before login event
if (settings.login && $.isFunction(settings.onBeforeLogin)) {
try {
settings.onBeforeLogin(self);
} catch (e) {
display_exception(e, 'onBeforeLogin');
throw e;
}
}
var auth = settings.login;
// create json-rpc authentication function
var base_interpreter;
if (typeof init_interpreter == 'string') {
base_interpreter = init_interpreter;
} else if (init_interpreter instanceof Array &&
typeof init_interpreter[0] == 'string') {
base_interpreter = init_interpreter[0];
}
if (base_interpreter &&
(typeof settings.login === 'string' || settings.login)) {
settings.login = make_json_rpc_login(base_interpreter,
settings.login);
}
terminals.append(self);
var interpreters;
var command_line;
var old_enabled;
function focus_terminal() {
if (old_enabled) {
self.focus();
}
}
function blur_terminal() {
old_enabled = enabled;
self.disable();
}
make_interpreter(init_interpreter, !!settings.login, function(itrp) {
if (settings.completion && typeof settings.completion != 'boolean') {
//overwrite interpreter completion by global setting #224
itrp.completion = settings.completion;
}
interpreters = new Stack($.extend({
name: settings.name,
prompt: settings.prompt,
keypress: settings.keypress,
keydown: settings.keydown,
resize: settings.onResize,
greetings: settings.greetings,
mousewheel: settings.mousewheel
}, itrp));
// CREATE COMMAND LINE
command_line = $('<div/>').appendTo(self).cmd({
prompt: settings.prompt,
history: settings.history,
historyFilter: settings.historyFilter,
historySize: settings.historySize,
width: '100%',
enabled: enabled && !is_touch,
keydown: key_down,
keypress: function(e) {
var result, i, top = interpreters.top();
if ($.isFunction(top.keypress)) {
return top.keypress(e, self);
} else if ($.isFunction(settings.keypress)) {
return settings.keypress(e, self);
}
},
onCommandChange: function(command) {
if ($.isFunction(settings.onCommandChange)) {
try {
settings.onCommandChange(command, self);
} catch (e) {
display_exception(e, 'onCommandChange');
throw e;
}
}
scroll_to_bottom();
},
commands: commands
});
// touch devices need touch event to get virtual keyboard
if (enabled && self.is(':visible') && !is_touch) {
self.focus(undefined, true);
} else {
self.disable();
}
self.oneTime(100, function() {
function disable(e) {
var sender = $(e.target);
if (!sender.closest('.terminal').length &&
self.enabled() &&
settings.onBlur(self) !== false) {
self.disable();
}
}
$(document).bind('click.terminal', disable).
bind('contextmenu.terminal', disable);
});
if (!is_touch) {
// work weird on mobile
var $win = $(window).on('focus', focus_terminal).
on('blur', blur_terminal);
} else {
/*
self.find('textarea').on('blur.terminal', function() {
if (enabled) {
self.focus(false);
}
});*/
}
self.click(function(e) {
if (!self.enabled()) {
self.focus();
} else if (is_touch) {
// keep focusing silently so textarea get focus
self.focus(true, true);
}
// this will ensure that textarea has focus
command_line.enable();
}).delegate('.exception a', 'click', function(e) {
//.on('click', '.exception a', function(e) {
// in new jquery .delegate just call .on
var href = $(this).attr('href');
if (href.match(/:[0-9]+$/)) { // display line if specified
e.preventDefault();
print_line(href);
}
});
if (!navigator.platform.match(/linux/i)) {
// on linux system paste work with middle mouse button
self.mousedown(function(e) {
if (e.which == 2) {
var selected = get_selected_text();
self.insert(selected);
}
});
}
if (self.is(':visible')) {
num_chars = self.cols();
command_line.resize(num_chars);
num_rows = get_num_rows(self);
}
// -------------------------------------------------------------
// Run Login
if (settings.login) {
self.login(settings.login, true, initialize);
} else {
initialize();
}
self.oneTime(100, function() {
$win.bind('resize.terminal', function() {
if (self.is(':visible')) {
var width = self.width();
var height = self.height();
// prevent too many calculations in IE
if (old_height !== height || old_width !== width) {
self.resize();
}
}
});
});
// -------------------------------------------------------------
// :: helper
function exec_spec(spec) {
var terminal = terminals.get()[spec[0]];
// execute if belong to this terminal
if (terminal && terminal_id == terminal.id()) {
if (spec[2]) {
try {
if (paused) {
var defer = $.Deferred();
resume_callbacks.push(function() {
return terminal.exec(spec[2]).then(function(term, i) {
terminal.save_state(spec[2], true, spec[1]);
defer.resolve();
});
});
return defer.promise();
} else {
return terminal.exec(spec[2]).then(function(term, i) {
terminal.save_state(spec[2], true, spec[1]);
});
}
} catch (e) {
var cmd = $.terminal.escape_brackets(command);
var msg = "Error while exec with command " + cmd;
terminal.error(msg).exception(e);
}
}
}
}
// exec from hash called in each terminal instance
if (settings.execHash) {
if (location.hash) {
try {
var hash = location.hash.replace(/^#/, '');
// yes no var - global inside terminal
hash_commands = $.parseJSON(decodeURIComponent(hash));
var i = 0;
(function recur() {
var spec = hash_commands[i++];
if (spec) {
exec_spec(spec).then(recur);
} else {
change_hash = true;
}
})();//*/
} catch (e) {
//invalid json - ignore
}
} else {
change_hash = true;
}
} else {
change_hash = true; // if enabled later
}
//change_hash = true; // exec can now change hash
// -------------------------------------------------------------
if ($.event.special.mousewheel) {
var shift = false;
$(document).bind('keydown.terminal', function(e) {
if (e.shiftKey) {
shift = true;
}
}).bind('keyup.terminal', function(e) {
// in Google Chromium/Linux shiftKey is false
if (e.shiftKey || e.which == 16) {
shift = false;
}
});
self.mousewheel(function(event, delta) {
if (!shift) {
var interpreter = interpreters.top();
if ($.isFunction(interpreter.mousewheel)) {
var ret = interpreter.mousewheel(event, delta, self);
if (ret === false) {
return;
}
} else if ($.isFunction(settings.mousewheel)) {
settings.mousewheel(event, delta, self);
}
if (delta > 0) {
self.scroll(-40);
} else {
self.scroll(40);
}
//event.preventDefault();
}
});
}
}); // make_interpreter
self.data('terminal', self);
return self;
}; //terminal plugin
})(jQuery);