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;
});