Created
September 2, 2016 16:12
-
-
Save jcubic/ce02aab838b9bdbb72761e20223f9b63 to your computer and use it in GitHub Desktop.
Development version of jQuery Terminal without content editable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/**@license | |
* __ _____ ________ __ | |
* / // _ /__ __ _____ ___ __ _/__ ___/__ ___ ______ __ __ __ ___ / / | |
* __ / // // // // // _ // _// // / / // _ // _// // // \/ // _ \/ / | |
* / / // // // // // ___// / / // / / // ___// / / / / // // /\ // // / /__ | |
* \___//____ \\___//____//_/ _\_ / /_//____//_/ /_/ /_//_//_/ /_/ \__\_\___/ | |
* \/ /____/ version {{VER}} | |
* | |
* 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: {{DATE}} | |
*/ | |
/* 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.filter(Boolean).length) { | |
return; | |
} | |
if (data.length === 1) { | |
return data[0]; | |
} else { | |
if (pos === data.length - 1) { | |
pos = 0; | |
} else { | |
++pos; | |
} | |
if (data[pos]) { | |
return data[pos]; | |
} else { | |
return 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; | |
var restart = false; | |
while (!data[index]) { | |
index++; | |
if (index > data.length) { | |
if (restart) { | |
break; | |
} | |
index = 0; | |
restart = true; | |
} | |
} | |
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"> </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); | |
// is it needed? | |
//var contentEditable = $('<div contentEditable></div>') | |
//$(document.body).append(contentEditable); | |
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; | |
var paste_count = 0; | |
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 blink'); | |
self.everyTime(500, 'blink', blink); | |
} else if (animating && !toggle) { | |
animating = false; | |
self.stopTime('blink', blink); | |
cursor.removeClass('inverted blink'); | |
} | |
}; | |
} | |
// --------------------------------------------------------------------- | |
// :: 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[0].getBoundingClientRect().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(' '); | |
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(' '); | |
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) { | |
if (paste_count++ > 0) { | |
return; | |
} | |
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 | |
self.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 | |
paste_count = 0; | |
if (!is_paste_supported) { | |
paste(e); | |
} 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 || e.which === 118) { // CTRL+V | |
clip.val(''); | |
paste_count = 0; | |
if (!is_paste_supported) { | |
paste(e); | |
} else { | |
clip.focus(); | |
clip.on('input', function input(e) { | |
paste(e); | |
clip.off('input', input); | |
}); | |
} | |
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.metaKey) && ([99, 118, 86].indexOf(e.which) !== -1)) { | |
// CTRL+C or CTRL+V | |
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?:\/\/(?:(?:(?!&[^;]+;)|(?=&))[^\s"'<>\]\[)])+\b)/gi; | |
var url_nf_re = /\b(https?:\/\/(?:(?:(?!&[^;]+;)|(?=&))[^\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: '{{VER}}', | |
// 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, ']').replace(/\n/g, '\\n'). | |
replace(/ /g, ' '); | |
return '[[' + format + semicolons + safe + ']' + text + ']'; | |
}).split(/\n/g); | |
function is_space() { | |
return line.substring(j-6, j) == ' ' || | |
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(/( |\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, '&'); | |
return str.replace(/</g, '<').replace(/>/g, '>') | |
.replace(/ /g, ' ') | |
.replace(/\t/g, ' '); | |
}, | |
// --------------------------------------------------------------------- | |
// :: 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)) { | |
// fix inside formatting because encode is called | |
// before format | |
text = text.replace(/\[\[[^\]]+\]/, function(text) { | |
return text.replace(/ /g, ' '); | |
}); | |
return text.replace(format_parts_re, function(s, | |
style, | |
color, | |
background, | |
_class, | |
data_text, | |
text) { | |
if (text === '') { | |
return ''; //'<span> </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(/]/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('"', '"e;') + '">' + | |
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, '[').replace(/\]/g, ']'); | |
}, | |
// --------------------------------------------------------------------- | |
// :: 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"> </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 wrap"><span class="cursor">' + | |
' </span></div>').appendTo('body').css('padding', 0); | |
var span = temp.find('span'); | |
var width = span[0].getBoundingClientRect().width; | |
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 = 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('[RPC] ' + error.message); | |
if (error.error && error.error.message) { | |
error = error.error; | |
// more detailed error message | |
var msg = '\t' + error.message; | |
if (error.file) { | |
msg += ' in file "' + error.file.replace(/.*\//, '') + '"'; | |
} | |
if (error.at) { | |
msg += ' at line ' + error.at; | |
} | |
self.error(msg); | |
} | |
} | |
} | |
// --------------------------------------------------------------------- | |
// :: 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('[AUTH] ' + | |
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('[AUTH] ' + strings.noTokenError); | |
return; | |
} | |
}*/ | |
var val = object[command.name]; | |
var type = $.type(val); | |
if (type === 'function') { | |
if (arity && val.length !== command.args.length) { | |
self.error("[Arity] " + | |
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('[AJAX] ' + 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("[Arity] " + | |
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('[AUTH] ' + | |
strings.noTokenError); | |
} | |
} | |
$.jrpc(url, proc.name, args, 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); | |
} | |
}; | |
}); | |
interpreter_object.help = interpreter_object.help || function(fn) { | |
if (typeof fn == 'undefined') { | |
self.echo('Available commands: ' + ret.procs.map(function(proc) { | |
return proc.name; | |
}).join(', ') + ', help'); | |
} else { | |
var found = false; | |
$.each(ret.procs, function(_, proc) { | |
if (proc.name == fn) { | |
found = true; | |
var msg = ''; | |
msg += '[[bu;#fff;]' + proc.name + ']'; | |
if (proc.params) { | |
msg += ' ' + proc.params.join(' '); | |
} | |
if (proc.help) { | |
msg += '\n' + proc.help; | |
} | |
self.echo(msg); | |
return false; | |
} | |
}); | |
if (!found) { | |
if (fn == 'help') { | |
self.echo('[[bu;#fff;]help] [method]\ndisplay help ' + | |
'for the method or list of methods if not'+ | |
' specified'); | |
} else { | |
var msg = 'Method `' + fn.toString() + '\' not found ';//' | |
self.error(msg); | |
} | |
} | |
} | |
}; | |
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 object; | |
var result = {}; | |
var rpc_count = 0; // only one rpc can be use for array | |
var fn_interpreter; | |
if (type === 'array') { | |
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) { | |
object = { | |
interpreter: make_basic_json_rpc(user_intrp, login) | |
}; | |
if ($.isArray(settings.completion)) { | |
object.completion = settings.completion; | |
} | |
finalize(object); | |
} 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); | |
} | |
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'); | |
} | |
} | |
clear_loging_storage(); | |
if ($.isFunction(settings.onAfterLogout)) { | |
try { | |
settings.onAfterLogout(self); | |
} catch (e) { | |
display_exception(e, 'onAfterlogout'); | |
} | |
} | |
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(), completion; | |
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; | |
} | |
} | |
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 cmd_strings = command.split(' '); | |
var string; // string before cursor that will be completed | |
if (strings.length == 1) { | |
string = cmd_strings[0]; | |
} else { | |
string = cmd_strings[cmd_strings.length-1]; | |
for (i=cmd_strings.length-1; i>0; i--) { | |
// treat escape space as part of the string | |
if (cmd_strings[i-1][cmd_strings[i-1].length-1] == '\\') { | |
string = cmd_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 === 118 || e.which === 86) && | |
(e.ctrlKey || e.metaKey)) { // 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 init_deferr = $.Deferred(); | |
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; | |
var autologin = true; // set to false of onBeforeLogin return 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'); | |
} | |
self.attr({ scrollTop: 0}); | |
return self; | |
}, | |
// ------------------------------------------------------------- | |
// :: Return an object that can be used with import_view to | |
// :: restore the state | |
// ------------------------------------------------------------- | |
export_view: function() { | |
var user_export = {}; | |
if ($.isFunction(settings.onExport)) { | |
try { | |
user_export = settings.onExport(); | |
} catch(e) { | |
display_exception(e, 'onExport'); | |
} | |
} | |
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() | |
}, user_export); | |
}, | |
// ------------------------------------------------------------- | |
// :: 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)) { | |
try { | |
settings.onImport(view); | |
} catch(e) { | |
display_exception(e, 'onImport'); | |
} | |
} | |
init_deferr.then(function() { | |
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) { | |
var d = deferred || new $.Deferred(); | |
function run() { | |
if ($.isArray(command)) { | |
(function recur() { | |
var cmd = command.shift(); | |
if (cmd) { | |
self.exec(cmd, silent).then(recur); | |
} else { | |
d.resolve(); | |
} | |
})(); | |
} else if (paused) { | |
// both commands executed here (resume will call Term::exec) | |
// 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); | |
}); | |
} | |
} | |
// while testing it didn't executed last exec when using this | |
// for resolved deferred | |
if (init_deferr.state() != 'resolved') { | |
init_deferr.then(run); | |
} else { | |
run(); | |
} | |
return d.promise(); | |
}, | |
// ------------------------------------------------------------- | |
// :: bypass login function that wait untill 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); | |
} | |
in_login = true; | |
if (self.token() && self.level() == 1 && !autologin) { | |
in_login = false; // logout will call login | |
self.logout(true); | |
} else { | |
if (self.token(true) && self.login_name(true)) { | |
in_login = false; | |
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(); | |
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!'); | |
} | |
return 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(); | |
} | |
return self; | |
}, | |
// ------------------------------------------------------------- | |
// :: 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) { | |
init_deferr.then(function() { | |
paused = true; | |
command_line.disable().hidden(); | |
if ($.isFunction(settings.onPause)) { | |
settings.onPause(); | |
} | |
}); | |
} | |
return self; | |
}, | |
// ------------------------------------------------------------- | |
// :: Resume the previously paused terminal | |
// ------------------------------------------------------------- | |
resume: function() { | |
function run() { | |
paused = false; | |
if (terminals.front() == self) { | |
command_line.enable(); | |
} | |
command_line.visible(); | |
var original = delayed_commands; | |
delayed_commands = []; | |
for (var i = 0; i < original.length; ++i) { | |
self.exec.apply(self, original[i]); | |
} | |
self.trigger('resume'); | |
var fn = resume_callbacks.shift(); | |
if (fn) { | |
fn(); | |
} | |
scroll_to_bottom(); | |
if ($.isFunction(settings.onResume)) { | |
settings.onResume(); | |
} | |
} | |
if (paused && command_line) { | |
if (init_deferr.state() != 'resolved') { | |
init_deferr.then(run); | |
} else { | |
run(); | |
} | |
} | |
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'); | |
} | |
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) { | |
init_deferr.then(function() { | |
if (terminals.length() === 1) { | |
if (toggle === false) { | |
try { | |
if (!silent && settings.onBlur(self) !== false || | |
silent) { | |
self.disable(); | |
} | |
} catch (e) { | |
display_exception(e, 'onBlur'); | |
} | |
} else { | |
try { | |
if (!silent && settings.onFocus(self) !== false || | |
silent) { | |
self.enable(); | |
} | |
} catch (e) { | |
display_exception(e, 'onFocus'); | |
} | |
} | |
} 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'); | |
} | |
} | |
} | |
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) { | |
init_deferr.then(function() { | |
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(); | |
} | |
init_deferr.then(function() { | |
command_line.enable(); | |
enabled = true; | |
}); | |
} | |
return self; | |
}, | |
// ------------------------------------------------------------- | |
// :: Disable the terminal | |
// ------------------------------------------------------------- | |
disable: function() { | |
if (enabled && !frozen) { | |
init_deferr.then(function() { | |
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) { | |
init_deferr.then(function() { | |
command_line.set(command); | |
}); | |
return self; | |
}, | |
// ------------------------------------------------------------- | |
// :: Insert text into the command line after the cursor | |
// ------------------------------------------------------------- | |
insert: function(string) { | |
if (typeof string === 'string') { | |
init_deferr.then(function() { | |
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) { | |
init_deferr.then(function() { | |
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) { | |
init_deferr.then(function() { | |
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) { | |
init_deferr.then(function() { | |
if (line < 0) { | |
line = lines.length + line; // yes + | |
} | |
if (!lines[line]) { | |
self.error('Invalid line number ' + line); | |
} else { | |
if (string == null) { | |
lines.splice(line, 1); | |
} 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; | |
}, | |
// ------------------------------------------------------------- | |
// :: return index of last line in case when you need to update | |
// :: after something is echo on the terminal | |
// ------------------------------------------------------------- | |
last_index: function() { | |
return lines.length-1; | |
}, | |
// ------------------------------------------------------------- | |
// :: 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(/\\$/, '\'). | |
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 = '[' + label + ']: ' + 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) { | |
var stack = $.terminal.escape_brackets(e.stack); | |
self.echo(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, 'logout')); | |
} | |
init_deferr.then(function() { | |
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) { | |
init_deferr.then(function() { | |
options = options || {}; | |
var defaults = { | |
infiniteLogin: false | |
}; | |
var settings = $.extend({}, defaults, options); | |
if (!settings.name && prev_command) { | |
// push is called in login | |
settings.name = prev_command.name; | |
} | |
if (settings.prompt === undefined) { | |
settings.prompt = (settings.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, settings)); | |
if ($.isArray(ret.completion) && settings.completion === true) { | |
interpreters.top().completion = ret.completion; | |
} else if (!ret.completion && settings.completion === true) { | |
interpreters.top().completion = false; | |
} | |
if (settings.login) { | |
var type = $.type(settings.login); | |
if (type == 'function') { | |
// self.pop on error | |
self.login(settings.login, | |
settings.infiniteLogin, | |
prepare_top_interpreter, | |
settings.infiniteLogin ? $.noop : self.pop); | |
} else if ($.type(interpreter) == 'string' && | |
type == 'string' || type == 'boolean') { | |
self.login(make_json_rpc_login(interpreter, | |
settings.login), | |
settings.infiniteLogin, | |
prepare_top_interpreter, | |
settings.infiniteLogin ? $.noop : 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'); | |
} | |
} | |
return true; | |
} else { | |
self.error(strings.canExitError); | |
} | |
} else { | |
if (self.token(true)) { | |
clear_loging_storage(); | |
} | |
var current = interpreters.pop(); | |
prepare_top_interpreter(); | |
// we check in case if you don't pop from password interpreter | |
if (in_login && self.get_prompt() != strings.login + ': ') { | |
in_login = false; | |
} | |
if ($.isFunction(current.onExit)) { | |
try { | |
current.onExit(self); | |
} catch (e) { | |
display_exception(e, 'onExit'); | |
} | |
} | |
// 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() { | |
init_deferr.then(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() { | |
init_deferr.then(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() { | |
init_deferr.then(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 { | |
if (settings.onBeforeLogin(self) === false) { | |
autologin = false; | |
} | |
} 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) { | |
// first JSON-RPC | |
for (var i=0, len=init_interpreter.length; i<len; ++i) { | |
if (typeof init_interpreter[i] == 'string') { | |
base_interpreter = init_interpreter[i]; | |
break; | |
} | |
} | |
} | |
if (base_interpreter && | |
(typeof settings.login === 'string' || settings.login === true)) { | |
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' || | |
!settings.completion) { | |
//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); | |
}); | |
var $win = $(window); | |
if (!is_touch) { | |
// work weird on mobile | |
$win.on('focus', focus_terminal). | |
on('blur', blur_terminal); | |
} else { | |
/* | |
self.find('textarea').on('blur.terminal', function() { | |
if (enabled) { | |
self.focus(false); | |
} | |
});*/ | |
} | |
// detect mouse drag | |
(function() { | |
var isDragging = false; | |
self.mousedown(function() { | |
$(window).mousemove(function() { | |
isDragging = true; | |
$(window).unbind('mousemove'); | |
}); | |
}).mouseup(function() { | |
var wasDragging = isDragging; | |
isDragging = false; | |
$(window).unbind('mousemove'); | |
if (!wasDragging) { | |
if (!self.enabled()) { | |
self.focus(); | |
} else if (is_touch) { | |
// keep focusing silently so textarea get focus | |
self.focus(true, true); | |
} | |
command_line.enable(); | |
} | |
}); | |
})(); | |
self.click(function(e) { | |
// 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(); | |
} | |
}); | |
} | |
init_deferr.resolve(); | |
}); // make_interpreter | |
self.data('terminal', self); | |
return self; | |
}; //terminal plugin | |
})(jQuery); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment