diff --git a/plugins/cockpit/lib/cli.js b/plugins/cockpit/lib/cli.js index eeeb289a..cfdbcc00 100644 --- a/plugins/cockpit/lib/cli.js +++ b/plugins/cockpit/lib/cli.js @@ -103,7 +103,7 @@ Hint.sort = function(hints, cursor) { } } // otherwise go with hint severity - return hint1 - hint2; + return hint2.status - hint1.status; }); // tidy-up if (cursor !== undefined) { diff --git a/plugins/cockpit/lib/ui/plain.js b/plugins/cockpit/lib/ui/plain.js index 9df2bfb4..f1d2f81d 100644 --- a/plugins/cockpit/lib/ui/plain.js +++ b/plugins/cockpit/lib/ui/plain.js @@ -50,6 +50,8 @@ var Templater = require("pilot/domtemplate").Templater; var canon = require("pilot/canon"); var Status = require('pilot/types').Status; +var NO_HINT = new Hint(Status.VALID, '', 0, 0); + /** * On startup we need to: * 1. Add 3 sets of elements to the DOM for: @@ -59,117 +61,156 @@ var Status = require('pilot/types').Status; * 2. Attach a set of events so the command line works */ exports.startup = function(data, reason) { - // TODO: We should probably cut this up into an object - var settings = data.env.settings; - var doc = document; - var win = doc.defaultView; - var cli = new CliRequisition(); + var plainUi = new PlainUi(data); +}; + +/** + * A class to handle the simplest UI implementation + */ +function PlainUi(data) { + this.doc = document; + this.win = this.doc.defaultView; // TODO: we should have a better way to specify command lines??? - var input = doc.getElementById('cockpit'); - if (!input) { + this.input = this.doc.getElementById('cockpit'); + if (!this.input) { console.log('No element with an id of cockpit. Bailing on plain cli'); return; } - var templates = doc.createElement('div'); - templates.innerHTML = plainRow; - var row = templates.firstChild; + this.cli = new CliRequisition(); - var completer = doc.createElement('div'); - completer.className = 'cptCompletion VALID'; - input.parentNode.insertBefore(completer, input); + this.settings = data.env.settings; + this.showHint = this.settings.getSetting('showHint'); + this.outputHeight = this.settings.getSetting('outputHeight'); - var hinter = doc.createElement('div'); - hinter.className = 'cptHints'; - input.parentNode.insertBefore(hinter, input); + this.hints = []; + this.shownHint; + this.worstHint; - var output = doc.createElement('div'); - output.className = 'cptOutput'; - input.parentNode.insertBefore(output, input); + this.createElements(); +} +PlainUi.prototype = { + /** + * Create divs for templates, completion, hints and output + */ + createElements: function() { + this.templates = this.doc.createElement('div'); + this.templates.innerHTML = plainRow; + this.row = this.templates.firstChild; - function resizer() { + this.completer = this.doc.createElement('div'); + this.completer.className = 'cptCompletion VALID'; + this.input.parentNode.insertBefore(this.completer, this.input); + + this.hinter = this.doc.createElement('div'); + this.hinter.className = 'cptHints'; + this.input.parentNode.insertBefore(this.hinter, this.input); + + this.output = this.doc.createElement('div'); + this.output.className = 'cptOutput'; + this.input.parentNode.insertBefore(this.output, this.input); + + this.win.addEventListener('resize', this.resizer.bind(this), false); + this.resizer(); + + canon.addEventListener('output', this.showOutput.bind(this)); + + this.showHint.addEventListener('change', this.hintShower.bind(this)); + this.hintShower(); + + keyutil.addKeyDownListener(this.input, this.onKeyDown.bind(this)); + this.input.addEventListener('mouseup', this.onMouseUp.bind(this), false); + this.input.addEventListener('keyup', this.onKeyUp.bind(this), true); + }, + + /** + * To be called on window resize or any time we want to align the elements + * with the input box. + */ + resizer: function() { var top, height, left, width; - if (input.getClientRects) { - var rect = input.getClientRects()[0]; + if (this.input.getClientRects) { + var rect = this.input.getClientRects()[0]; top = rect.top; height = rect.height; left = rect.left; width = rect.width; } else { - var style = win.getComputedStyle(input, null); + var style = this.win.getComputedStyle(this.input, null); top = parseInt(style.getPropertyValue('top'), 10); height = parseInt(style.getPropertyValue('height'), 10); left = parseInt(style.getPropertyValue('left'), 10); width = parseInt(style.getPropertyValue('width'), 10); } - completer.style.top = top + 'px'; - completer.style.height = height + 'px'; - completer.style.left = left + 'px'; - completer.style.width = width + 'px'; + this.completer.style.top = top + 'px'; + this.completer.style.height = height + 'px'; + this.completer.style.left = left + 'px'; + this.completer.style.width = width + 'px'; - hinter.style.bottom = (win.innerHeight - top) + 'px'; - hinter.style.left = (left + 30) + 'px'; + this.hinter.style.bottom = (this.win.innerHeight - top) + 'px'; + this.hinter.style.left = (left + 30) + 'px'; - output.style.bottom = (win.innerHeight - top) + 'px'; - output.style.left = left + 'px'; - output.style.width = width + 'px'; - } + this.output.style.bottom = (this.win.innerHeight - top) + 'px'; + this.output.style.left = left + 'px'; + this.output.style.width = width + 'px'; + }, - win.addEventListener('resize', resizer.bind(this), true); - resizer(); + /** + * Update the display of executed commands + */ + showOutput: function(ev) { + // TODO: be less brutal in how we update this + this.output.innerHTML = ''; - // TODO: be less brutal in how we update this - output.innerHTML = ''; - canon.addEventListener('output', function(ev) { ev.requests.forEach(function(request) { request.outputs.forEach(function(out) { if (typeof out === 'string') { - output.appendChild(doc.createTextNode(out)); + this.output.appendChild(this.doc.createTextNode(out)); } else { - output.appendChild(out); + this.output.appendChild(out); } }, this); }, this); - }.bind(this)); + }, - var showHint = settings.getSetting('showHint'); - function hintShower() { - if (showHint.get()) { - hinter.style.display = 'block'; + /** + * Show/hide the hint line. + * It's not clear that this is actually useful, however it does help to + * highlight some features for right now. + * TODO: remove this? + */ + hintShower: function() { + if (this.showHint.get()) { + this.hinter.style.display = 'block'; } else { - hinter.style.display = 'none'; + this.hinter.style.display = 'none'; } - } - hintShower(); - showHint.addEventListener('change', hintShower.bind(this)); + }, - var outputHeight = settings.getSetting('outputHeight'); - - /* - // All this does is to kill TABs normal use. I wonder if we can train - // people to use right arrow? Probably not? but ... - keyutil.addKeyDownListener(input, function(ev) { - // env.commandLine = this; + /** + * Ensure that TAB isn't handled by the browser + */ + onKeyDown: function(ev) { + var handled; // var handled = keyboardManager.processKeyEvent(ev, this, { // isCommandLine: true, isKeyUp: false // }); if (ev.keyCode === keyutil.KeyHelper.KEY.TAB) { return true; } - //return handled; - }.bind(this)); - */ + return handled; + }, - var NO_HINT = new Hint(Status.VALID, '', 0, 0); - var hints = []; - var worst; - - input.addEventListener('keyup', function(ev) { + /** + * The main keyboard processing loop + */ + onKeyUp: function(ev) { + var handled; /* var handled = keyboardManager.processKeyEvent(ev, this, { isCommandLine: true, isKeyUp: true @@ -177,94 +218,140 @@ exports.startup = function(data, reason) { */ if (ev.keyCode === keyutil.KeyHelper.KEY.RETURN) { - cli.exec(); - input.value = ''; - } else { - cli.update({ - typed: input.value, - cursor: { - start: input.selectionStart, - end: input.selectionEnd - } - }); - - completer.classList.remove(Status.VALID.toString()); - completer.classList.remove(Status.INCOMPLETE.toString()); - completer.classList.remove(Status.INVALID.toString()); - - // TODO: borked implementation? - // dom.removeCssClass(completer, Status.VALID.toString()); - // dom.removeCssClass(completer, Status.INCOMPLETE.toString()); - // dom.removeCssClass(completer, Status.INVALID.toString()); - - hints = cli.getHints(); - - // Create a marked up version of the input - var highlightedInput = ''; - if (input.value.length > 0) { - // 'scores' is an array which tells us what chars are errors - // Initialize with everything VALID - var scores = input.value.split('').map(function(char) { - return Status.VALID; - }); - // For all chars in all hints, check and upgrade the score - hints.forEach(function(hint) { - for (var i = hint.start; i <= hint.end; i++) { - if (hint.status > scores[i]) { - scores[i] = hint.status; - } - } - }, this); - // Create markup - var i = 0; - var lastStatus = -1; - while (true) { - if (lastStatus !== scores[i]) { - highlightedInput += ''; - lastStatus = scores[i]; - } - highlightedInput += input.value[i]; - i++; - if (i === input.value.length) { - highlightedInput += ''; - break; - } - if (lastStatus !== scores[i]) { - highlightedInput += ''; - } - } - } - - worst = (hints.length > 0) ? hints[0] : NO_HINT; - var message = worst.message; - if (worst.predictions && worst.predictions.length > 0) { - message += ': [ '; - worst.predictions.forEach(function(prediction) { - if (prediction.name) { - message += prediction.name + ' | '; - } - else { - message += prediction + ' | '; - } - }, this); - message = message.replace(/\| $/, ']'); - - var completion = worst.predictions[0]; - completion = completion.name ? completion.name : completion; - completer.innerHTML = highlightedInput + '  -> ' + completion; + if (this.worstHint && this.worstHint.status !== Status.VALID) { + this.input.selectionStart = this.worstHint.start; + this.input.selectionEnd = this.worstHint.end; } else { - completer.innerHTML = highlightedInput; + this.cli.exec(); + this.input.value = ''; } - hinter.innerHTML = message; - - completer.classList.add(worst.status.toString()); - // dom.addCssClass(input, worst.status.toString()); } - // return handled; - }.bind(this), true); + if (ev.keyCode === keyutil.KeyHelper.KEY.TAB) { + if (this.shownHint && this.shownHint.predictions && this.shownHint.predictions.length > 0) { + var prefix = this.input.value.substring(0, this.shownHint.start); + var suffix = this.input.value.substring(this.shownHint.end); + var insert = this.shownHint.predictions[0]; + insert = typeof insert === 'string' ? insert : insert.name; + this.input.value = prefix + insert + suffix; + // Fix the cursor. + var insertEnd = (prefix + insert).length; + this.input.selectionStart = insertEnd; + this.input.selectionEnd = insertEnd; + } + } + + this.update(); + + return handled; + }, + + /** + * Cause an update if the cursor changes position due to a mouse click + * TODO: there are probably some performance wins here. + */ + onMouseUp: function(ev) { + this.update(); + }, + + /** + * Actually parse the input and make sure we're all up to date + */ + update: function() { + this.cli.update({ + typed: this.input.value, + cursor: { + start: this.input.selectionStart, + end: this.input.selectionEnd + } + }); + + // TODO: borked implementation? This is modern browser only. Fix + this.completer.classList.remove(Status.VALID.toString()); + this.completer.classList.remove(Status.INCOMPLETE.toString()); + this.completer.classList.remove(Status.INVALID.toString()); + // dom.removeCssClass(completer, Status.VALID.toString()); + // dom.removeCssClass(completer, Status.INCOMPLETE.toString()); + // dom.removeCssClass(completer, Status.INVALID.toString()); + + this.hints = this.cli.getHints(); + + // Those hints came in order of display importance - i.e. an INCOMPLETE + // hint under the cursor should be displayed before an INVALID hint + // somewhere else. That's good for displaying hints, but not good for + // deciding if we're good to go. + if (this.hints.length > 1) { + hintClone = this.hints.slice(0); + this.worstHint = Hint.sort(hintClone)[0]; + } + + // Create a marked up version of the input + var highlightedInput = ''; + if (this.input.value.length > 0) { + // 'scores' is an array which tells us what chars are errors + // Initialize with everything VALID + var scores = this.input.value.split('').map(function(char) { + return Status.VALID; + }); + // For all chars in all hints, check and upgrade the score + this.hints.forEach(function(hint) { + for (var i = hint.start; i <= hint.end; i++) { + if (hint.status > scores[i]) { + scores[i] = hint.status; + } + } + }, this); + // Create markup + var i = 0; + var lastStatus = -1; + while (true) { + if (lastStatus !== scores[i]) { + highlightedInput += ''; + lastStatus = scores[i]; + } + highlightedInput += this.input.value[i]; + i++; + if (i === this.input.value.length) { + highlightedInput += ''; + break; + } + if (lastStatus !== scores[i]) { + highlightedInput += ''; + } + } + } + + // Display the "-> prediction" at the end of the completer + this.shownHint = (this.hints.length > 0) ? this.hints[0] : NO_HINT; + var message = this.shownHint.message; + if (this.shownHint.predictions && this.shownHint.predictions.length > 0) { + message += ': [ '; + this.shownHint.predictions.forEach(function(prediction) { + if (prediction.name) { + message += prediction.name + ' | '; + } + else { + message += prediction + ' | '; + } + }, this); + message = message.replace(/\| $/, ']'); + + var onTab = this.shownHint.predictions[0]; + onTab = onTab.name ? onTab.name : onTab; + this.completer.innerHTML = highlightedInput + '  -> ' + onTab; + } + else { + this.completer.innerHTML = highlightedInput; + } + this.hinter.innerHTML = message; + + var status = this.worstHint ? this.worstHint.status : Status.VALID; + this.completer.classList.add(status.toString()); + // dom.addCssClass(input, status.toString()); + } }; +exports.PlainUi = PlainUi; });