opencloud shell prototype; wip
diff --git a/planetstack/core/dashboard/shell/opencloud_shell.js b/planetstack/core/dashboard/shell/opencloud_shell.js
new file mode 100644
index 0000000..634788a
--- /dev/null
+++ b/planetstack/core/dashboard/shell/opencloud_shell.js
@@ -0,0 +1,268 @@
+// TryMongo
+//
+// Copyright (c) 2009 Kyle Banker
+// Licensed under the MIT Licence.
+// http://www.opensource.org/licenses/mit-license.php
+
+// Readline class to handle line input.
+var ReadLine = function(options) {
+  this.options      = options || {};
+  this.htmlForInput = this.options.htmlForInput;
+  this.inputHandler = this.options.handler || this.mockHandler;
+  this.scoper       = this.options.scoper;
+  this.terminal     = $(this.options.terminalId || "#terminal");
+  this.lineClass    = this.options.lineClass || '.readLine';
+  this.history      = [];
+  this.historyPtr   = 0;
+
+  this.initialize();
+};
+
+ReadLine.prototype = {
+
+  initialize: function() {
+    this.addInputLine();
+  },
+
+  // Enter a new input line with proper behavior.
+  addInputLine: function(stackLevel) {
+    stackLevel = stackLevel || 0;
+    this.terminal.append(this.htmlForInput(stackLevel));
+    var ctx = this;
+    ctx.activeLine = $(this.lineClass + '.active');
+
+    // Bind key events for entering and navigting history.
+    ctx.activeLine.bind("keydown", function(ev) {
+      switch (ev.keyCode) {
+        case EnterKeyCode:
+          ctx.processInput(this.value); 
+          break;
+        case UpArrowKeyCode: 
+          ctx.getCommand('previous');
+          break;
+        case DownArrowKeyCode: 
+          ctx.getCommand('next');
+          break;
+      }
+    });
+
+    this.activeLine.focus();
+  },
+
+  // Returns the 'next' or 'previous' command in this history.
+  getCommand: function(direction) {
+    if(this.history.length === 0) {
+      return;
+    }
+    this.adjustHistoryPointer(direction);
+    this.activeLine[0].value = this.history[this.historyPtr];
+    $(this.activeLine[0]).focus();
+    //this.activeLine[0].value = this.activeLine[0].value;
+  },
+
+  // Moves the history pointer to the 'next' or 'previous' position. 
+  adjustHistoryPointer: function(direction) {
+    if(direction == 'previous') {
+      if(this.historyPtr - 1 >= 0) {
+        this.historyPtr -= 1;
+      }
+    }
+    else {
+      if(this.historyPtr + 1 < this.history.length) {
+        this.historyPtr += 1;
+      }
+    }
+  },
+
+  // Return the handler's response.
+  processInput: function(value) {
+    var response = this.inputHandler.apply(this.scoper, [value]);
+    this.insertResponse(response.result);
+
+    // Save to the command history...
+    if((lineValue = value.trim()) !== "") {
+      this.history.push(lineValue);
+      this.historyPtr = this.history.length;
+    }
+
+    // deactivate the line...
+    this.activeLine.value = "";
+    this.activeLine.attr({disabled: true});
+    this.activeLine.removeClass('active');
+
+    // and add add a new command line.
+    this.addInputLine(response.stack);
+  },
+
+  insertResponse: function(response) {
+    if(response.length < 3) {
+      this.activeLine.parent().append("<p class='response'></p>");
+    }
+    else {
+      this.activeLine.parent().append("<p class='response'>" + response + "</p>");
+    }
+  },
+
+  // Simply return the entered string if the user hasn't specified a smarter handler.
+  mockHandler: function(inputString) {
+    return function() {
+      this._process = function() { return inputString; };
+    };
+  }
+};
+
+var MongoHandler = function() {
+  this._currentCommand = "";
+  this._rawCommand     = "";
+  this._commandStack   = 0;
+  this._tutorialPtr    = 0;
+  this._tutorialMax    = 10;
+
+  this._mongo          = {};
+  this._mongo.test     = [];
+  this.collections     = [];
+};
+
+MongoHandler.prototype = {
+
+  _process: function(inputString, errorCheck) {
+    this._rawCommand += ' ' + inputString;
+
+//    try {
+      inputString += '  '; // fixes certain bugs with the tokenizer.
+      var tokens    = inputString.tokens();
+      var mongoFunc = this._getCommand(tokens);
+      if(this._commandStack === 0 && inputString.match(/^\s*$/)) {
+        return {stack: 0, result: ''};
+      }
+      else if(this._commandStack === 0 && mongoFunc) {
+        this._resetCurrentCommand();
+        return {stack: 0, result: mongoFunc.apply(this, [tokens])};
+      }
+      else {
+        return this._evaluator(tokens);
+      }
+//    }
+
+//    catch(err) {
+//        this._resetCurrentCommand();
+//        return {stack: 0, result: "JS Error: " + err};
+//    }
+  },
+
+  // Calls eval on the input string when ready.
+  _evaluator: function(tokens) {
+    this._currentCommand += " " + this._massageTokens(tokens);
+    if(this._shouldEvaluateCommand(tokens))  {
+        db = "scott";
+        print = this.print;
+
+        // So this eval statement is the heart of the REPL.
+        var result = eval(this._currentCommand.trim());
+        if(result === undefined) {
+          throw('result is undefined');
+        } else {
+          result = $htmlFormat(result);
+        }
+        this._resetCurrentCommand();
+        console.log(result);
+        return {stack: this._commandStack, result: result};
+      }
+
+    else {
+      return {stack: this._commandStack, result: ""};
+    }
+  },
+
+  _resetCurrentCommand: function() {
+    this._currentCommand = '';
+    this._rawCommand     = '';
+  },
+
+  // Evaluate only when we've exited any blocks.
+  _shouldEvaluateCommand: function(tokens) {
+    for(var i=0; i < tokens.length; i++) {
+      var token = tokens[i];
+      if(token.type == 'operator') {
+        if(token.value == '(' || token.value == '{') {
+          this._commandStack += 1;
+        }
+        else if(token.value == ')' || token.value == '}') {
+          this._commandStack -= 1;
+        }
+      }
+    }
+
+    if(this._commandStack === 0) {
+      return true;
+    }
+    else {
+      return false;
+    }
+  },
+
+  _massageTokens: function(tokens) {
+    for(var i=0; i < tokens.length; i++) {
+      if(tokens[i].type == 'name') {
+        if(tokens[i].value == 'var') {
+          tokens[i].value = '';
+        }
+      }
+    }
+    return this._collectTokens(tokens);
+  },
+
+  // Collects tokens into a string, placing spaces between variables.
+  // This methods is called after we scope the vars.
+  _collectTokens: function(tokens) {
+    var result = "";
+    for(var i=0; i < tokens.length; i++) {
+      if(tokens[i].type == "name" && tokens[i+1] && tokens[i+1].type == 'name') {
+        result += tokens[i].value + ' ';
+      }
+      else if (tokens[i].type == 'string') {
+        result += "'" + tokens[i].value + "'";
+      }
+      else {
+        result += tokens[i].value;
+      }
+    }
+    return result;
+  },
+
+  // print output to the screen, e.g., in a loop
+  // TODO: remove dependency here
+  print: function() {
+   $('.readLine.active').parent().append('<p>' + arguments[0] + '</p>');
+   return "";
+  },
+
+  /* MongoDB     */
+  /* ________________________________________ */
+
+  // help command
+  _help: function() {
+      return PTAG('HELP');
+
+  },
+
+  _getCommand: function(tokens) {
+    if(tokens[0] && MongoKeywords.include((tokens[0].value + '').toLowerCase())) {
+      switch(tokens[0].value.toLowerCase()) {
+        case 'help':
+          return this._help;
+      }
+    }
+  }
+};
+
+$htmlFormat = function(obj) {
+  return tojson(obj, ' ', ' ', true);
+}
+
+$(document).ready(function() {
+  var mongo       = new MongoHandler();
+  var terminal    = new ReadLine({htmlForInput: DefaultInputHtml,
+                                  handler: mongo._process,
+                                  scoper: mongo});
+});