make the command line ui process hints at least to a basic extent

This commit is contained in:
Joe Walker 2010-12-09 18:22:44 +00:00
commit 1e65d71bde
12 changed files with 911 additions and 90 deletions

View file

@ -70,6 +70,50 @@ function Hint(status, message, start, end) {
}
Hint.prototype = {
};
/**
* Loop over the array of hints finding the one we should display.
* @param hints array of hints
*/
Hint.worst = function(hints, cursor) {
if (hints.length === 0) {
return undefined;
}
// Calculate 'distance from cursor'
if (cursor !== undefined) {
hints.forEach(function(hint) {
if (cursor < hint.start) {
hint.distance = hint.start - cursor;
}
else {
if (cursor > hint.end) {
hint.distance = cursor - hint.end;
}
else {
hint.distance = 0;
}
}
}, this);
}
// Sort
hints.sort(function(hint1, hint2) {
// Compare first based on distance from cursor
if (cursor !== undefined) {
var diff = hint1.distance - hint2.distance;
if (diff != 0) {
return diff;
}
}
// otherwise go with hint severity
return hint1 - hint2;
});
// tidy-up
if (cursor !== undefined) {
hints.forEach(function(hint) {
delete hint.distance;
}, this);
}
return hints[0];
};
exports.Hint = Hint;
/**
@ -816,6 +860,4 @@ function documentCommand(command) {
exports.documentCommand = documentCommand;
});

View file

@ -1,2 +1,181 @@
#cockpit { color: red; }
/* Command line completion */
.cptCompletion {
color: #666;
padding: 0 0 5px 2px;
position: absolute;
z-index: -1000;
}
.cptHints {
color: #000;
position: absolute;
border: 1px solid rgba(230, 230, 230, 0.8);
background: rgba(250, 250, 250, 0.8);
-moz-border-radius-topleft: 10px;
-moz-border-radius-topright: 10px;
border-top-left-radius: 10px; border-top-right-radius: 10px;
z-index: 1000;
padding: 8px;
}
.cptCompletion.VALID { background: rgba(210, 255, 210, 1); }
.cptCompletion.INCOMPLETE { background: rgba(255, 230, 210, 1); }
.cptCompletion.INVALID { background: rgba(255, 210, 210, 1); }
.cptCompletion span { color: #FFF; }
.cptCompletion span.INCOMPLETE { text-shadow: 0px 0px 2px #ff8800; }
.cptCompletion span.INVALID { text-shadow: 0px 0px 5px #ff0000; }
#cockpit, .cptCompletion {
font-family: consolas, courier, monospace;
font-size: 120%;
}
#cockpit {
background: transparent;
border: none; outline: none;
}
.cptHints article {
margin-left: 25px;
padding: 8px;
z-index: 1;
color: @text;
display: block;
border: 2px @border_bg solid;
border-bottom: 0;
font-size: 90%;
-moz-border-top-colors: @border_bg @border_fg;
-moz-border-left-colors: @border_bg @border_fg;
-moz-border-right-colors: @border_bg @bg;
-moz-border-radius-topleft: 10px;
-moz-border-radius-topright: 10px;
border-top-left-radius: 10px; border-top-right-radius: 10px;
background: @input_bg2;
background: -moz-linear-gradient(top, @input_bg_light, @input_bg);
background: -webkit-gradient(linear, left top, left bottom, from(@input_bg_light), to(@input_bg));
-webkit-box-shadow: rgba(0, 0, 0, 0.5) 0px 1px 10px 3px;
-moz-box-shadow: rgba(0, 0, 0, 0.5) 0px 1px 10px 3px;
max-width: 500px;
}
.cptHints article h1 { font-size: 110%; text-align: center; }
.cptHints article h2 { font-size: 95%; }
.cptHints article h3 { font-size: 90%; }
.cptHints article table th,
.cptHints article label { color: @hi_text; }
.cptHints article h1,
.cptHints article h2,
.cptHints article h3 { margin: 2px 0 1px 0; color: @hi_text; }
.cptHints article p,
.cptHints article pre,
/* Global visual styles */
.cptBrackets { color: @hi_text; }
.cptGt { color: @theme_text; font-weight: bold; font-size: 120%; }
/* Use absolute positioning to put all children 100%x100% */
.cptStack {
position: relative;
}
.cptStack > * {
position: absolute;
top: 0; height: 100%; bottom: 0; left: 0; width: 100%; right: 0;
}
/* Layout */
.cptOutput {
background: @bg; color: @text;
font-family: @fonts; font-size: 90%;
overflow: hidden;
position: absolute;
}
/* The input area */
.cptInput {
border-top: 1px solid @border_bg;
background: @input_bg;
background: -moz-linear-gradient(top, @input_bg, @input_bg2);
background: -webkit-gradient(linear, left top, left bottom, from(@input_bg), to(@input_bg2));
position: absolute;
height: 25px; right: 0; bottom: 0; left: 0;
}
.cptPrompt {
width: 40px; padding-left: 5px;
position: absolute;
top: 0; width: 40px; bottom: 0; left: 0;
}
.cptKbd {
position: absolute;
top: 0; right: 0; bottom: 0; left: 40px;
}
.cptOutput input.cptCliInput {
color: @hi_text; font-size: 120%; font-family: @fonts;
background: transparent;
outline: none; border: 0;
box-shadow: none; -moz-box-shadow: none; -webkit-box-shadow: none;
padding: 4px 0 0 0;
width: 100%;
}
.cptOutput input.cptCliInput:focus {
outline: none; border: 0;
box-shadow: none; -moz-box-shadow: none; -webkit-box-shadow: none;
}
.cptRowIn {
display: box; display: -moz-box; display: -webkit-box;
box-orient: horizontal; -moz-box-orient: horizontal; -webkit-box-orient: horizontal;
box-align: center; -moz-box-align: center; -webkit-box-align: center;
color: @text;
background-color: @input_bg;
width: 100%;
}
.cptRowIn > * { padding-left: 2px; padding-right: 2px; }
.cptRowIn > img { cursor: pointer; }
.cptHover { display: none; }
.cptRowIn:hover > .cptHover { display: block; }
.cptRowIn:hover > .cptHover.cptHidden { display: none; }
.cptOutTyped {
box-flex: 1; -moz-box-flex: 1; -webkit-box-flex: 1;
font-weight: bold; color: @hi_text; font-size: 120%;
}
.cptRowOutput { padding-left: 10px; line-height: 1.2em; }
.cptRowOutput strong,
.cptRowOutput b,
.cptRowOutput th,
.cptRowOutput h1,
.cptRowOutput h2,
.cptRowOutput h3 { color: @hi_text; }
.cptRowOutput a { font-weight: bold; color: @link_text; text-decoration: none; }
.cptRowOutput a: hover { text-decoration: underline; cursor: pointer; }
.cptRowOutput input[type=password],
.cptRowOutput input[type=text],
.cptRowOutput textarea {
color: @hi_text; font-size: 120%;
background: transparent; padding: 3px;
border-radius: 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px;
}
.cptRowOutput table,
.cptRowOutput td,
.cptRowOutput th { border: 0; padding: 0 2px; }
.cptRowOutput .right { text-align: right; }

View file

@ -42,17 +42,88 @@ var dom = require("pilot/dom").dom;
dom.importCssString(editorCss);
var CliRequisition = require('cockpit/cli').CliRequisition;
var Hint = require('cockpit/cli').Hint;
var keyutil = require('pilot/keyboard/keyutil');
var plainRow = require("text!cockpit/ui/plainRow.html");
var Templater = require("pilot/domtemplate").Templater;
var canon = require("pilot/canon");
var Status = require('pilot/types').Status;
/**
* On startup we need to:
* 1. Add 3 sets of elements to the DOM for:
* - command line output
* - input hints
* - completion
* 2. Attach a set of events so the command line works
*/
exports.startup = function(data, reason) {
var doc = document;
var win = doc.defaultView;
var cli = new CliRequisition();
// TODO: we should have a better way to specify command lines???
this.input = document.getElementById('cockpit');
if (!this.input) {
var input = doc.getElementById('cockpit');
if (!input) {
console.log('No element with an id of cockpit. Bailing on plain cli');
return;
}
var cli = new CliRequisition();
var templates = doc.createElement('dic');
templates.innerHTML = plainRow;
var row = templates.firstChild;
var completer = doc.createElement('div');
completer.className = 'cptCompletion VALID';
input.parentNode.insertBefore(completer, input);
var hinter = doc.createElement('div');
hinter.className = 'cptHints';
input.parentNode.insertBefore(hinter, input);
var output = doc.createElement('div');
output.className = 'cptOutput';
input.parentNode.insertBefore(output, input);
function resizer() {
var style = win.getComputedStyle(input, null);
var top = parseInt(style.getPropertyValue('top'), 10);
var height = parseInt(style.getPropertyValue('height'), 10);
var left = parseInt(style.getPropertyValue('left'), 10);
var 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';
hinter.style.bottom = (win.innerHeight - top) + 'px';
hinter.style.left = (left + 30) + 'px';
output.style.bottom = (win.innerHeight - top) + 'px';
output.style.left = left + 'px';
output.style.width = width + 'px';
}
win.addEventListener('resize', resizer.bind(this), true);
resizer();
// 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));
} else {
output.appendChild(out);
}
}, this);
}, this);
}.bind(this));
/*
// All this does is to kill TABs normal use. I wonder if we can train
@ -69,7 +140,11 @@ exports.startup = function(data, reason) {
}.bind(this));
*/
this.input.addEventListener('keyup', function(ev) {
var NO_HINT = new Hint(Status.VALID, '', 0, 0);
var hints = [];
var worst;
input.addEventListener('keyup', function(ev) {
/*
var handled = keyboardManager.processKeyEvent(ev, this, {
isCommandLine: true, isKeyUp: true
@ -78,16 +153,88 @@ exports.startup = function(data, reason) {
if (ev.keyCode === keyutil.KeyHelper.KEY.RETURN) {
cli.exec();
this.input.value = '';
input.value = '';
} else {
cli.update({
typed: this.input.value,
typed: input.value,
cursor: {
start: this.input.selectionStart,
end: this.input.selectionEnd
start: input.selectionStart,
end: input.selectionEnd
}
});
console.log(JSON.stringify(cli.getHints()));
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 += '<span class=' + scores[i].toString() + '>';
lastStatus = scores[i];
}
highlightedInput += input.value[i];
i++;
if (i === input.value.length) {
highlightedInput += '</span>';
break;
}
if (lastStatus !== scores[i]) {
highlightedInput += '</span>';
}
}
}
worst = Hint.worst(hints) || 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 + ' &nbsp;-&gt; ' + completion;
}
else {
completer.innerHTML = highlightedInput;
}
hinter.innerHTML = message;
completer.classList.add(worst.status.toString());
// dom.addCssClass(input, worst.status.toString());
}
// return handled;

View file

@ -0,0 +1,47 @@
<!-- The input area, with fixed height -->
<div id=cptInput class=cptInput>
<!-- The prompt -->
<div class="cptPrompt cptGt"><span class="cptBrackets">{ }</span> &gt;</div>
<!-- Where you type commands -->
<div class="cptKbd cptStack">
<div class=cptCompletion save="${cliInputView._completer}"></div>
<input class=cptCliInput save="${cliInputView._inputer}"/>
</div>
<!-- Help as you type -->
<div id=cptHints class=cptHints>
</div>
</div>
<div class=cptRow>
<!-- The div for the input (i.e. what was typed) -->
<div class="cptRowIn" save="${actions.rowin}"
onclick="${actions.copyToInput}"
ondblclick="${actions.executeRequest}">
<!-- What the user actually typed -->
<div class="cptGt">&gt; </div>
<div class="cptOutTyped" save="${actions.typed}"></div>
<!-- The extra details that appear on hover -->
<div class=cptHover save="${actions.duration}"></div>
<img class=cptHover onclick="${actions.hideOutput}" save="${actions.hide}"
alt="Hide command output" _src="${imagePath}/minus.png"/>
<img class="cptHover cptHidden" onclick="${actions.showOutput}" save="${actions.show}"
alt="Show command output" _src="${imagePath}/plus.png"/>
<img class=cptHover onclick="${actions.remove}"
alt="Remove this command from the history" _src="${imagePath}/closer.png"/>
</div>
<!-- The div for the command output -->
<div class="cptRowOut" save="${actions.rowout}">
<div class="cptRowOutput" save="${actions.output}"></div>
<img src="${imagePath}/throbber.gif" save="${actions.throb}"/>
</div>
</div>