Created
November 2, 2013 18:05
-
-
Save Buildstarted/7281714 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
///<reference path="typings/jquery/jquery.d.ts"/> | |
///<reference path="typings/signalr/signalr.d.ts"/> | |
module T { | |
export class CommandAdapter { | |
public OnPrint: any; | |
public Proxy: HubProxy; | |
public Connection: HubConnection; | |
constructor() { | |
this.Initialize($.connection.hub, (<any>$.connection).command); | |
} | |
public Initialize(connection: HubConnection, proxy: HubProxy): void { | |
this.Connection = connection; | |
this.Proxy = proxy; | |
var savedProxyInvoke = this.Proxy.invoke; | |
this.OnPrint = new Array(); | |
(<any>this.Proxy.invoke) = () => { | |
if ((<any>this.Connection).state === $.signalR.connectionState.connected) { | |
return savedProxyInvoke.apply(this.Proxy, arguments); | |
} | |
}; | |
this.Wire(); | |
} | |
private Wire(): void { | |
this.Proxy.on("print", (data) => { | |
if (this.OnPrint) { | |
for (var i in this.OnPrint) { | |
this.OnPrint[i](data); | |
} | |
} | |
}); | |
} | |
} | |
export class Config { | |
public ScrollStep: number; | |
public ScrollSpeed: number; | |
public BackgroundColor: string; | |
public ForegroundColor: string; | |
public CursorBlinkRate: number; | |
public CursorStyle: string; | |
public Prompt: string; | |
public SpinnerCharacters: string[]; | |
public SpinnerSpeed: number; | |
public TypingSpeed: number; | |
constructor() { | |
this.ScrollStep = 20; | |
this.ScrollSpeed = 100; | |
this.BackgroundColor = '#000'; | |
this.ForegroundColor = '#c0c0c0'; | |
this.CursorBlinkRate = 700; | |
this.CursorStyle = 'block'; | |
this.Prompt = 'admin@cmd=/$ '; | |
this.SpinnerCharacters = ['[ ]', '[. ]', '[.. ]', '[...]']; | |
this.SpinnerSpeed = 250; | |
this.TypingSpeed = 50; | |
} | |
} | |
export class StickyKeys { | |
public Keys: any = { | |
ctrl: false, | |
alt: false, | |
scroll: false | |
}; | |
constructor(private $: JQuery) { } | |
private set(key, state): void { | |
this.Keys[key] = state; | |
$('#' + key + '-indicator').toggle(this.Keys[key]); | |
} | |
public Toggle(key): void { | |
this.set(key, !this.Keys[key]); | |
} | |
public Reset(key): void { | |
this.set(key, false); | |
} | |
public ResetAll(): void { | |
$.each(this.Keys, $.proxy(function (name, value): void { | |
this.Reset(name); | |
}, this)); | |
} | |
} | |
export class TerminalShell { | |
private _buffer: string = ''; | |
private _pos: number = 0; | |
private _history: any[]; | |
private _historyPos: number = 0; | |
private _promptActive: boolean = true; | |
private _cursorBlinkState: boolean = true; | |
private _cursorBlinkTimeout: any = null; | |
private _spinnerIndex: number = 0; | |
private _spinnerTimeout: any = null; | |
private _config: Config = new Config(); | |
private _sticky: StickyKeys; | |
private _commandAdapter: CommandAdapter; | |
constructor(private $: JQuery) { | |
this._config = new Config(); | |
this._sticky = new StickyKeys($); | |
this._history = new Array(); | |
this._commandAdapter = new CommandAdapter(); | |
this._commandAdapter.OnPrint.push((e) => { | |
this.print(e); | |
}); | |
} | |
public Init(): void { | |
var self = this; | |
function ifActive(func) { | |
return function () { | |
if (self._promptActive) { | |
func.apply(this, arguments); | |
} | |
}; | |
} | |
$(document) | |
.keypress($.proxy(ifActive((e) => { | |
if (e.which >= 32 && e.which <= 126) { | |
var character = String.fromCharCode(e.which); | |
var letter = character.toLowerCase(); | |
} else { | |
return; | |
} | |
if ((<any>$).browser.opera && !(/[\w\s]/.test(character))) { | |
return; // sigh. | |
} | |
if (this._sticky.Keys.ctrl) { | |
if (letter == 'w') { | |
this.deleteWord(); | |
} else if (letter == 'h') { | |
this.deleteCharacter(false); | |
} else if (letter == 'l') { | |
this.clear(); | |
} else if (letter == 'a') { | |
this.setPos(0); | |
} else if (letter == 'e') { | |
this.setPos(this._buffer.length); | |
} else if (letter == 'd') { | |
this.runCommand('logout'); | |
} | |
} else { | |
if (character) { | |
this.addCharacter(character); | |
e.preventDefault(); | |
} | |
} | |
}), this)) | |
.bind('keydown', 'return', ifActive((e) => { this.processInputBuffer(); })) | |
.bind('keydown', 'backspace', ifActive((e) => { e.preventDefault(); this.deleteCharacter(e.shiftKey); })) | |
.bind('keydown', 'del', ifActive((e) => { this.deleteCharacter(true); })) | |
.bind('keydown', 'left', ifActive((e) => { this.moveCursor(-1); })) | |
.bind('keydown', 'right', ifActive((e) => { this.moveCursor(1); })) | |
.bind('keydown', 'up', ifActive((e) => { | |
e.preventDefault(); | |
if (e.shiftKey || this._sticky.Keys.scroll) { | |
this.scrollLine(- 1); | |
} else if (e.ctrlKey || this._sticky.Keys.ctrl) { | |
this.scrollPage(-1); | |
} else { | |
this.moveHistory(-1); | |
} | |
})) | |
.bind('keydown', 'down', ifActive((e) => { | |
e.preventDefault(); | |
if (e.shiftKey || this._sticky.Keys.scroll) { | |
this.scrollLine(1); | |
} else if (e.ctrlKey || this._sticky.Keys.ctrl) { | |
this.scrollPage(1); | |
} else { | |
this.moveHistory(1); | |
} | |
})) | |
.bind('keydown', 'pageup', ifActive((e) => { this.scrollPage(-1); })) | |
.bind('keydown', 'pagedown', ifActive((e) => { this.scrollPage(1); })) | |
.bind('keydown', 'home', ifActive((e) => { | |
e.preventDefault(); | |
if (e.ctrlKey || this._sticky.Keys.ctrl) { | |
this.jumpToTop(); | |
} else { | |
this.setPos(0); | |
} | |
})) | |
.bind('keydown', 'end', ifActive((e) => { | |
e.preventDefault(); | |
if (e.ctrlKey || this._sticky.Keys.ctrl) { | |
this.jumpToBottom(); | |
} else { | |
this.setPos(this._buffer.length); | |
} | |
})) | |
.bind('keydown', 'tab', (e) => { | |
e.preventDefault(); | |
}) | |
.keyup((e) => { | |
var keyName = (<any>$).hotkeys.specialKeys[e.which]; | |
if (keyName in { 'ctrl': true, 'alt': true, 'scroll': true }) { | |
this._sticky.Toggle(keyName); | |
} else if (!(keyName in { 'left': true, 'right': true, 'up': true, 'down': true })) { | |
this._sticky.ResetAll(); | |
} | |
}); | |
$(window).resize((e) => { | |
$('#screen').scrollTop(parseInt($('#screen').attr('scrollHeight'))); | |
}); | |
this.setCursorState(true); | |
this.setWorking(false); | |
$('#prompt').html(this._config.Prompt); | |
$('#screen').hide().fadeIn('fast', function () { | |
$('#screen').triggerHandler('cli-load'); | |
}); | |
this._promptActive = true; | |
} | |
private setCursorState(state, fromTimeout: any = null): void { | |
this._cursorBlinkState = state; | |
if (this._config.CursorStyle == 'block') { | |
if (state) { | |
$('#cursor').css({ color: this._config.BackgroundColor, backgroundColor: this._config.ForegroundColor }); | |
} else { | |
$('#cursor').css({ color: this._config.ForegroundColor, background: 'none' }); | |
} | |
} else { | |
if (state) { | |
$('#cursor').css('textDecoration', 'underline'); | |
} else { | |
$('#cursor').css('textDecoration', 'none'); | |
} | |
} | |
// (Re)schedule next blink. | |
if (!fromTimeout && this._cursorBlinkTimeout) { | |
window.clearTimeout(this._cursorBlinkTimeout); | |
this._cursorBlinkTimeout = null; | |
} | |
this._cursorBlinkTimeout = window.setTimeout($.proxy(function () { | |
this.setCursorState(!this.cursorBlinkState, true); | |
}, this), this._config.CursorBlinkRate); | |
} | |
private updateInputDisplay(): void { | |
var left = '', underCursor = ' ', right = ''; | |
if (this._pos < 0) { | |
this._pos = 0; | |
} | |
if (this._pos > this._buffer.length) { | |
this._pos = this._buffer.length; | |
} | |
if (this._pos > 0) { | |
left = this._buffer.substr(0, this._pos); | |
} | |
if (this._pos < this._buffer.length) { | |
underCursor = this._buffer.substr(this._pos, 1); | |
} | |
if (this._buffer.length - this._pos > 1) { | |
right = this._buffer.substr(this._pos + 1, this._buffer.length - this._pos - 1); | |
} | |
$('#lcommand').text(left); | |
$('#cursor').text(underCursor); | |
if (underCursor == ' ') { | |
$('#cursor').html(' '); | |
} | |
$('#rcommand').text(right); | |
$('#prompt').text(this._config.Prompt); | |
return; | |
} | |
private clearInputBuffer(): void { | |
this._buffer = ''; | |
this._pos = 0; | |
this.updateInputDisplay(); | |
} | |
private clear(): void { | |
$('#display').html(''); | |
} | |
private addCharacter(character): void { | |
var left = this._buffer.substr(0, this._pos); | |
var right = this._buffer.substr(this._pos, this._buffer.length - this._pos); | |
this._buffer = left + character + right; | |
this._pos++; | |
this.updateInputDisplay(); | |
this.setCursorState(true); | |
} | |
private deleteCharacter(forward): void { | |
var offset = forward ? 1 : 0; | |
if (this._pos >= (1 - offset)) { | |
var left = this._buffer.substr(0, this._pos - 1 + offset); | |
var right = this._buffer.substr(this._pos + offset, this._buffer.length - this._pos - offset); | |
this._buffer = left + right; | |
this._pos -= 1 - offset; | |
this.updateInputDisplay(); | |
} | |
this.setCursorState(true); | |
} | |
private deleteWord(): void { | |
if (this._pos > 0) { | |
var ncp = this._pos; | |
while (ncp > 0 && this._buffer.charAt(ncp) !== ' ') { | |
ncp--; | |
} | |
var left = this._buffer.substr(0, ncp - 1); | |
var right = this._buffer.substr(ncp, this._buffer.length - this._pos); | |
this._buffer = left + right; | |
this._pos = ncp; | |
this.updateInputDisplay(); | |
} | |
this.setCursorState(true); | |
} | |
private moveCursor(val): void { | |
this.setPos(this._pos + val); | |
} | |
private setPos(pos): void { | |
if ((pos >= 0) && (pos <= this._buffer.length)) { | |
this._pos = pos; | |
this.updateInputDisplay(); | |
} | |
this.setCursorState(true); | |
} | |
private moveHistory(val): void { | |
var newpos = this._historyPos + val; | |
if ((newpos >= 0) && (newpos <= this._history.length)) { | |
if (newpos == this._history.length) { | |
this.clearInputBuffer(); | |
} else { | |
this._buffer = this._history[newpos]; | |
} | |
this._pos = this._buffer.length; | |
this._historyPos = newpos; | |
this.updateInputDisplay(); | |
this.jumpToBottom(); | |
} | |
this.setCursorState(true); | |
} | |
private addHistory(cmd): void { | |
this._historyPos = this._history.push(cmd); | |
} | |
private jumpToBottom(): void { | |
$('#screen').animate({ scrollTop: $('#screen').attr('scrollHeight') }, this._config.ScrollSpeed, 'linear'); | |
} | |
private jumpToTop(): void { | |
$('#screen').animate({ scrollTop: 0 }, this._config.ScrollSpeed, 'linear'); | |
} | |
private scrollPage(num): void { | |
$('#screen').animate({ scrollTop: $('#screen').scrollTop() + num * ($('#screen').height() * .75) }, this._config.ScrollSpeed, 'linear'); | |
} | |
private scrollLine(num): void { | |
$('#screen').scrollTop($('#screen').scrollTop() + num * this._config.ScrollStep); | |
} | |
public print(text): void { | |
if (!text) { | |
$('#display').append($('<div>')); | |
} else if (text instanceof jQuery) { | |
$('#display').append(text); | |
} else { | |
var av = Array.prototype.slice.call(arguments, 0); | |
$('#display').append($('<p>').text(av.join(' '))); | |
} | |
this.jumpToBottom(); | |
} | |
// Removes leading whitespaces | |
private ltrim(value): string { | |
if (value) { | |
var re = /\s*((\S+\s*)*)/; | |
return value.replace(re, '$1'); | |
} | |
return ''; | |
} | |
// Removes ending whitespaces | |
private rtrim(value): string { | |
if (value) { | |
var re = /((\s*\S+)*)\s*/; | |
return value.replace(re, '$1'); | |
} | |
return ''; | |
} | |
// Removes leading and ending whitespaces | |
private trim(value): string { | |
if (value) { | |
return this.ltrim(this.rtrim(value)); | |
} | |
return ''; | |
} | |
private processInputBuffer(): any { | |
this.print($('<p>').addClass('command').text(this._config.Prompt + this._buffer)); | |
var cmd = this.trim(this._buffer); | |
this.clearInputBuffer(); | |
if (cmd.length == 0) { | |
return false; | |
} | |
this.addHistory(cmd); | |
//send command to server here | |
this._commandAdapter.Proxy.invoke("execute", cmd); | |
} | |
private setPromptActive(active): void { | |
this._promptActive = active; | |
$('#inputline').toggle(this._promptActive); | |
} | |
private setWorking(working): void { | |
if (working && !this._spinnerTimeout) { | |
$('#display .command:last-child').add('#bottomline').first().append($('#spinner')); | |
this._spinnerTimeout = window.setInterval($.proxy(function () { | |
if (!$('#spinner').is(':visible')) { | |
$('#spinner').fadeIn(); | |
} | |
this.spinnerIndex = (this.spinnerIndex + 1) % this._config.spinnerCharacters.length; | |
$('#spinner').text(this._config.spinnerCharacters[this.spinnerIndex]); | |
}, this), this._config.SpinnerSpeed); | |
this.setPromptActive(false); | |
$('#screen').triggerHandler('cli-busy'); | |
} else if (!working && this._spinnerTimeout) { | |
clearInterval(this._spinnerTimeout); | |
this._spinnerTimeout = null; | |
$('#spinner').fadeOut(); | |
this.setPromptActive(true); | |
$('#screen').triggerHandler('cli-ready'); | |
} | |
} | |
private runCommand(text): void { | |
var index = 0; | |
var mine = false; | |
this._promptActive = false; | |
var interval = window.setInterval($.proxy(function typeCharacter(): void { | |
if (index < text.length) { | |
this.addCharacter(text.charAt(index)); | |
index += 1; | |
} else { | |
clearInterval(interval); | |
this._promptActive = true; | |
this.processInputBuffer(); | |
} | |
}, this), this._config.TypingSpeed); | |
} | |
} | |
export var Terminal: TerminalShell; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment