diff --git a/js/web/shell.js b/js/web/shell.js
new file mode 100644
index 0000000..782528a
--- /dev/null
+++ b/js/web/shell.js
@@ -0,0 +1,394 @@
+/* ------------------------------------------------------------------------*
+ * Copyright 2013-2014 Arne F. Claassen
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *-------------------------------------------------------------------------*/
+
+var Josh = Josh || {};
+(function(root, $, _) {
+ Josh.Shell = function (config) {
+ config = config || {};
+
+ // instance fields
+ var _console = config.console || (Josh.Debug && root.console ? root.console : {
+ log: function () {
+ }
+ });
+ var _prompt = config.prompt || 'jsh$';
+ var _shell_view_id = config.shell_view_id || 'shell-view';
+ var _shell_panel_id = config.shell_panel_id || 'shell-panel';
+ var _input_id = config.input_id || 'shell-cli';
+ var _blinktime = config.blinktime || 500;
+ var _history = config.history || new Josh.History();
+ var _readline = config.readline || new Josh.ReadLine({history: _history, console: _console});
+ var _active = false;
+ var _cursor_visible = false;
+ var _activationHandler;
+ var _deactivationHandler;
+ var _cmdHandlerExt = false;
+ var _cmdHandlers = {
+ clear: {
+ exec: function (cmd, args, callback) {
+ $(id(_input_id)).parent().empty();
+ callback();
+ }
+ },
+ help: {
+ exec: function (cmd, args, callback) {
+ callback(self.templates.help({commands: commands()}));
+ }
+ },
+ history: {
+ exec: function (cmd, args, callback) {
+ if (args[0] == "-c") {
+ _history.clear();
+ callback();
+ return;
+ }
+ callback(self.templates.history({items: _history.items()}));
+ }
+ },
+ _default: {
+ exec: function (cmd, args, callback) {
+ callback(self.templates.bad_command({cmd: cmd}));
+ },
+ completion: function (cmd, arg, line, callback) {
+ if (!arg) {
+ arg = cmd;
+ }
+ return callback(self.bestMatch(arg, self.commands()))
+ }
+ }
+ };
+ var _line = {
+ text: '',
+ cursor: 0
+ };
+ var _searchMatch = '';
+ var _view, _panel;
+ var _promptHandler;
+ var _initializationHandler;
+ var _initialized;
+
+ // public methods
+ var self = {
+ commands: commands,
+ templates: {
+ history: _.template("
<% _.each(items, function(cmd, i) { %>
<%- i %> <%- cmd %>
<% }); %>
"),
+ help: _.template("Commands:
<% _.each(commands, function(cmd) { %>
<%- cmd %>
<% }); %>
"),
+ bad_command: _.template('Unrecognized command: <%=cmd%>
'),
+ input_cmd: _.template('
'),
+ input_search: _.template('(reverse-i-search)`\':
'),
+ suggest: _.template("<% _.each(suggestions, function(suggestion) { %>
<%- suggestion %>
<% }); %>
")
+ },
+ isActive: function () {
+ return _readline.isActive();
+ },
+ activate: function () {
+ if ($(id(_shell_view_id)).length == 0) {
+ _active = false;
+ return;
+ }
+ _readline.activate();
+ },
+ deactivate: function () {
+ _console.log("deactivating");
+ _active = false;
+ _readline.deactivate();
+ },
+ setCommandHandler: function (cmd, cmdHandler) {
+ if (cmd === '*') _cmdHandlerExt = true;
+ _cmdHandlers[cmd] = cmdHandler;
+ },
+ getCommandHandler: function (cmd) {
+ if (_cmdHandlerExt) return _cmdHandlers['*'];
+ else return _cmdHandlers[cmd];
+ },
+ setPrompt: function (prompt) {
+ _prompt = prompt;
+ if (!_active) {
+ return;
+ }
+ self.refresh();
+ },
+ onEOT: function (completionHandler) {
+ _readline.onEOT(completionHandler);
+ },
+ onCancel: function (completionHandler) {
+ _readline.onCancel(completionHandler);
+ },
+ onInitialize: function (completionHandler) {
+ _initializationHandler = completionHandler;
+ },
+ onActivate: function (completionHandler) {
+ _activationHandler = completionHandler;
+ },
+ onDeactivate: function (completionHandler) {
+ _deactivationHandler = completionHandler;
+ },
+ onNewPrompt: function (completionHandler) {
+ _promptHandler = completionHandler;
+ },
+ render: function () {
+ var text = _line.text || '';
+ var cursorIdx = _line.cursor || 0;
+ if (_searchMatch) {
+ cursorIdx = _searchMatch.cursoridx || 0;
+ text = _searchMatch.text || '';
+ $(id(_input_id) + ' .searchterm').text(_searchMatch.term);
+ }
+ var left = _.escape(text.substr(0, cursorIdx)).replace(/ /g, ' ');
+ var cursor = text.substr(cursorIdx, 1);
+ var right = _.escape(text.substr(cursorIdx + 1)).replace(/ /g, ' ');
+ $(id(_input_id) + ' .prompt').html(_prompt);
+ $(id(_input_id) + ' .input .left').html(left);
+ if (!cursor) {
+ $(id(_input_id) + ' .input .cursor').html(' ').css('textDecoration', 'underline');
+ } else {
+ $(id(_input_id) + ' .input .cursor').text(cursor).css('textDecoration', 'underline');
+ }
+ $(id(_input_id) + ' .input .right').html(right);
+ _cursor_visible = true;
+ self.scrollToBottom();
+ _console.log('rendered "' + text + '" w/ cursor at ' + cursorIdx);
+ },
+ refresh: function () {
+ $(id(_input_id)).replaceWith(self.templates.input_cmd({id: _input_id}));
+ self.render();
+ _console.log('refreshed ' + _input_id);
+
+ },
+ renderOutput: renderOutput,
+ scrollToBottom: function () {
+ _panel.animate({scrollTop: _view.height()}, 0);
+ },
+ bestMatch: function (partial, possible) {
+ _console.log("bestMatch on partial '" + partial + "'");
+ var result = {
+ completion: null,
+ suggestions: []
+ };
+ if (!possible || possible.length == 0) {
+ return result;
+ }
+ var common = '';
+ if (!partial) {
+ if (possible.length == 1) {
+ result.completion = possible[0];
+ result.suggestions = possible;
+ return result;
+ }
+ if (!_.every(possible, function (x) {
+ return possible[0][0] == x[0]
+ })) {
+ result.suggestions = possible;
+ return result;
+ }
+ }
+ for (var i = 0; i < possible.length; i++) {
+ var option = possible[i];
+ if (option.slice(0, partial.length) == partial) {
+ result.suggestions.push(option);
+ if (!common) {
+ common = option;
+ _console.log("initial common:" + common);
+ } else if (option.slice(0, common.length) != common) {
+ _console.log("find common stem for '" + common + "' and '" + option + "'");
+ var j = partial.length;
+ while (j < common.length && j < option.length) {
+ if (common[j] != option[j]) {
+ common = common.substr(0, j);
+ break;
+ }
+ j++;
+ }
+ }
+ }
+ }
+ result.completion = common.substr(partial.length);
+ return result;
+ }
+ };
+
+ function id(id) {
+ return "#" + id;
+ }
+
+ function commands() {
+ return _.chain(_cmdHandlers).keys().filter(function (x) {
+ return x[0] != "_"
+ }).value();
+ }
+
+ function blinkCursor() {
+ if (!_active) {
+ return;
+ }
+ root.setTimeout(function () {
+ if (!_active) {
+ return;
+ }
+ _cursor_visible = !_cursor_visible;
+ if (_cursor_visible) {
+ $(id(_input_id) + ' .input .cursor').css('textDecoration', 'underline');
+ } else {
+ $(id(_input_id) + ' .input .cursor').css('textDecoration', '');
+ }
+ blinkCursor();
+ }, _blinktime);
+ }
+
+ function split(str) {
+ return _.filter(str.split(/\s+/), function (x) {
+ return x;
+ });
+ }
+
+ function getHandler(cmd) {
+ if (_cmdHandlerExt) return _cmdHandlers[cmd] || _cmdHandlers['*'];
+ else return _cmdHandlers[cmd] || _cmdHandlers._default;
+ }
+
+ function renderOutput(output, callback) {
+ if (output) {
+ // TODO: workaround
+ $(id(_input_id)).last().css("visibility", "hidden");
+ $(id(_input_id)).last().css("height", "0");
+ $(id(_input_id)).after(output);
+ _console.log(output);
+ }
+ $(id(_input_id) + ' .input .cursor').css('textDecoration', '');
+ $(id(_input_id)).removeAttr('id');
+ $(id(_shell_view_id)).append(self.templates.input_cmd({id: _input_id}));
+ if (_promptHandler) {
+ return _promptHandler(function (prompt) {
+ self.setPrompt(prompt);
+ if (callback) return callback(); else return undefined;
+ });
+ }
+ if (callback) return callback(); else return undefined;
+ }
+
+ function activate() {
+ _console.log("activating shell");
+ if (!_view) {
+ _view = $(id(_shell_view_id));
+ }
+ if (!_panel) {
+ _panel = $(id(_shell_panel_id));
+ }
+ if ($(id(_input_id)).length == 0) {
+ _view.append(self.templates.input_cmd({id: _input_id}));
+ }
+ self.refresh();
+ _active = true;
+ blinkCursor();
+ if (_promptHandler) {
+ _promptHandler(function (prompt) {
+ self.setPrompt(prompt);
+ })
+ }
+ if (_activationHandler) {
+ _activationHandler();
+ }
+ }
+
+ // init
+ _readline.onActivate(function () {
+ if (!_initialized) {
+ _initialized = true;
+ if (_initializationHandler) {
+ return _initializationHandler(activate);
+ }
+ }
+ return activate();
+ });
+ _readline.onDeactivate(function () {
+ if (_deactivationHandler) {
+ _deactivationHandler();
+ }
+ });
+ _readline.onChange(function (line) {
+ _line = line;
+ self.render();
+ });
+ _readline.onClear(function () {
+ _cmdHandlers.clear.exec(null, null, function () {
+ renderOutput(null, function () {
+ });
+ });
+ });
+ _readline.onSearchStart(function () {
+ $(id(_input_id)).replaceWith(self.templates.input_search({id: _input_id}));
+ _console.log('started search');
+ });
+ _readline.onSearchEnd(function () {
+ $(id(_input_id)).replaceWith(self.templates.input_cmd({id: _input_id}));
+ _searchMatch = null;
+ self.render();
+ _console.log("ended search");
+ });
+ _readline.onSearchChange(function (match) {
+ _searchMatch = match;
+ self.render();
+ });
+ _readline.onEnter(function (cmdtext, callback) {
+ _console.log("got command: " + cmdtext);
+ var parts = split(cmdtext);
+ var cmd = parts[0];
+ var args = parts.slice(1);
+ var handler = getHandler(cmd);
+ return handler.exec(cmd, args, function (output, cmdtext) {
+ renderOutput(output, function () {
+ callback(cmdtext)
+ });
+ });
+ });
+ _readline.onCompletion(function (line, callback) {
+ if (!line) {
+ return callback();
+ }
+ var text = line.text.substr(0, line.cursor);
+ var parts = split(text);
+
+ var cmd = parts.shift() || '';
+ var arg = parts.pop() || '';
+ _console.log("getting completion handler for " + cmd);
+ var handler = getHandler(cmd);
+ if (handler != _cmdHandlers._default && cmd && cmd == text) {
+
+ _console.log("valid cmd, no args: append space");
+ // the text to complete is just a valid command, append a space
+ return callback(' ');
+ }
+ if (!handler.completion) {
+ // handler has no completion function, so we can't complete
+ return callback();
+ }
+ _console.log("calling completion handler for " + cmd);
+ return handler.completion(cmd, arg, line, function (match) {
+ _console.log("completion: " + JSON.stringify(match));
+ if (!match) {
+ return callback();
+ }
+ if (match.suggestions && match.suggestions.length > 1) {
+ return renderOutput(self.templates.suggest({suggestions: match.suggestions}), function () {
+ callback(match.completion);
+ });
+ }
+ return callback(match.completion);
+ });
+ });
+ return self;
+ }
+})(this, $, _);