Skip to content

Instantly share code, notes, and snippets.

@Buildstarted
Created November 2, 2013 18:05
Show Gist options
  • Save Buildstarted/7281714 to your computer and use it in GitHub Desktop.
Save Buildstarted/7281714 to your computer and use it in GitHub Desktop.
///<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('&nbsp;');
}
$('#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