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