1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-19 14:25:14 +03:00

Add auto completion

This commit is contained in:
Adem Gaygusuz 2018-05-29 22:53:35 +01:00
parent 65fd419649
commit 97775f2748
6 changed files with 3190 additions and 203 deletions

View file

@ -58,6 +58,10 @@ Linux build is disabled currently because of unmet dependecies with some distros
2. Change to project folder and run `npm install`. 2. Change to project folder and run `npm install`.
3. Run `npm start`. 3. Run `npm start`.
### Running tests
`npm test`
### App build and release ### App build and release
The tasks are defined in `gulpfile.js` and can be run either via `gulp <task-name>` (if the command is in PATH or via `../node_modules/gulp/bin/gulp.js <task-name>`: The tasks are defined in `gulpfile.js` and can be run either via `gulp <task-name>` (if the command is in PATH or via `../node_modules/gulp/bin/gulp.js <task-name>`:

2887
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,8 @@
"start": "node node_modules/gulp/bin/gulp.js debug", "start": "node node_modules/gulp/bin/gulp.js debug",
"_postinstall": "node ./node_modules/platform-dependent-modules/cli.js", "_postinstall": "node ./node_modules/platform-dependent-modules/cli.js",
"postinstall": "npm run _postinstall", "postinstall": "npm run _postinstall",
"gulp": "gulp" "gulp": "gulp",
"test": "karma start src/test/karma.conf.js"
}, },
"window": { "window": {
"show": false, "show": false,
@ -40,6 +41,7 @@
"marked": "^0.3.12" "marked": "^0.3.12"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.1.2",
"command-exists": "^1.2.2", "command-exists": "^1.2.2",
"del": "^3.0.0", "del": "^3.0.0",
"follow-redirects": "^1.4.1", "follow-redirects": "^1.4.1",
@ -52,11 +54,20 @@
"gulp-zip": "^4.1.0", "gulp-zip": "^4.1.0",
"inflection": "1.12.0", "inflection": "1.12.0",
"jquery-ui-npm": "1.12.0", "jquery-ui-npm": "1.12.0",
"karma": "^2.0.0",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^2.2.0",
"karma-mocha": "^1.3.0",
"karma-sinon": "^1.0.5",
"karma-sinon-chai": "^1.3.4",
"makensis": "^0.9.0", "makensis": "^0.9.0",
"mocha": "^5.2.0",
"nw-builder": "^3.4.1", "nw-builder": "^3.4.1",
"os": "^0.1.1", "os": "^0.1.1",
"platform-dependent-modules": "0.0.14", "platform-dependent-modules": "0.0.14",
"rpm-builder": "^0.7.0", "rpm-builder": "^0.7.0",
"sinon": "^5.0.10",
"sinon-chai": "^3.1.0",
"targz": "^1.0.1", "targz": "^1.0.1",
"temp": "^0.8.3" "temp": "^0.8.3"
}, },

View file

@ -1,14 +1,16 @@
'use strict'; 'use strict';
TABS.cli = { TABS.cli = {
'validateText': "",
'currentLine': "",
'sequenceElements': 0,
lineDelayMs: 15, lineDelayMs: 15,
profileSwitchDelayMs: 100, profileSwitchDelayMs: 100,
outputHistory: "" outputHistory: "",
cliBuffer: ""
}; };
function removePromptHash(promptText) {
return promptText.replace(/^# /, '');
}
TABS.cli.initialize = function (callback) { TABS.cli.initialize = function (callback) {
var self = this; var self = this;
@ -59,7 +61,7 @@ TABS.cli.initialize = function (callback) {
} else { } else {
console.log('write complete'); console.log('write complete');
}; };
} };
writer.truncate(0); writer.truncate(0);
}, function (){ }, function (){
@ -68,22 +70,73 @@ TABS.cli.initialize = function (callback) {
}); });
}); });
function cliBufferCharsToDelete(command, buffer) {
var commonChars = 0;
for (var i = 0;i < buffer.length;i++) {
if (command[i] === buffer[i]) {
commonChars++;
} else {
break;
}
}
return buffer.length - commonChars;
}
function commandWithBackSpaces(command, buffer, noOfCharsToDelete) {
const backspace = String.fromCharCode(127);
return backspace.repeat(noOfCharsToDelete) + command.substring(buffer.length - noOfCharsToDelete, command.length);
}
function getCliCommand(command, cliBuffer) {
const buffer = removePromptHash(cliBuffer);
const bufferRegex = new RegExp('^' + buffer, 'g');
if (command.match(bufferRegex)) {
return command.replace(bufferRegex, '');
}
const noOfCharsToDelete = cliBufferCharsToDelete(command, buffer);
return commandWithBackSpaces(command, buffer, noOfCharsToDelete);
}
// Tab key detection must be on keydown,
// `keypress`/`keyup` happens too late, as `textarea` will have already lost focus.
textarea.keydown(function (event) {
const tabKeyCode = 9;
if (event.which == tabKeyCode) {
// prevent default tabbing behaviour
event.preventDefault();
const outString = textarea.val();
const lastCommand = outString.split("\n").pop();
const command = getCliCommand(lastCommand, self.cliBuffer);
if (command) {
self.sendAutoComplete(command);
textarea.val('');
}
}
});
textarea.keypress(function (event) { textarea.keypress(function (event) {
if (event.which == 13) { // enter const enterKeyCode = 13;
if (event.which == enterKeyCode) {
event.preventDefault(); // prevent the adding of new line event.preventDefault(); // prevent the adding of new line
var out_string = textarea.val(); var out_string = textarea.val();
self.history.add(out_string.trim()); self.history.add(out_string.trim());
var outputArray = out_string.split("\n"); var outputArray = out_string.split("\n");
Promise.reduce(outputArray, function(delay, line) { Promise.reduce(outputArray, function(delay, line, index) {
return new Promise(function (resolve) { return new Promise(function (resolve) {
GUI.timeout_add('CLI_send_slowly', function () { GUI.timeout_add('CLI_send_slowly', function () {
var processingDelay = self.lineDelayMs; var processingDelay = self.lineDelayMs;
if (line.toLowerCase().startsWith('profile')) { if (line.toLowerCase().startsWith('profile')) {
processingDelay = self.profileSwitchDelayMs; processingDelay = self.profileSwitchDelayMs;
} }
const isLastCommand = index + 1 === outputArray.length;
if (isLastCommand && self.cliBuffer) {
line = getCliCommand(line, self.cliBuffer);
}
self.sendLine(line, function () { self.sendLine(line, function () {
resolve(processingDelay); resolve(processingDelay);
}); });
@ -134,15 +187,26 @@ TABS.cli.history.add = function (str) {
this.history.push(str); this.history.push(str);
this.index = this.history.length; this.index = this.history.length;
}; };
TABS.cli.history.prev = function () { TABS.cli.history.prev = function () {
if (this.index > 0) this.index -= 1; if (this.index > 0) this.index -= 1;
return this.history[this.index]; return this.history[this.index];
}; };
TABS.cli.history.next = function () { TABS.cli.history.next = function () {
if (this.index < this.history.length) this.index += 1; if (this.index < this.history.length) this.index += 1;
return this.history[this.index - 1]; return this.history[this.index - 1];
}; };
function writeToOutput(text) {
$('.tab-cli .window .wrapper').append(text);
$('.tab-cli .window').scrollTop($('.tab-cli .window .wrapper').height());
}
function writeToPrompt(text) {
$('.tab-cli textarea').val(text);
}
TABS.cli.read = function (readInfo) { TABS.cli.read = function (readInfo) {
/* Some info about handling line feeds and carriage return /* Some info about handling line feeds and carriage return
@ -152,106 +216,122 @@ TABS.cli.read = function (readInfo) {
MAC only understands CR MAC only understands CR
Linux and Unix only understand LF Linux and Unix only understand LF
Windows understands (both) CRLF Windows understands (both) CRLF
Chrome OS currenty unknown Chrome OS currently unknown
*/ */
var data = new Uint8Array(readInfo.data), var data = new Uint8Array(readInfo.data),
text = ""; cliOutput = "",
validateText = "",
sequenceCharsToSkip = 0;
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
if (CONFIGURATOR.cliValid) { const currentChar = String.fromCharCode(data[i]);
if (data[i] == 27 || this.sequenceElements > 0) { // ESC + other
this.sequenceElements++;
// delete previous space if (!CONFIGURATOR.cliValid) {
if (this.sequenceElements == 1) {
text = text.substring(0, text.length -1);
}
// Reset
if (this.sequenceElements >= 5) {
this.sequenceElements = 0;
}
}
if (this.sequenceElements == 0) {
switch (data[i]) {
case 10: // line feed
if (GUI.operating_system != "MacOS") {
text += "<br />";
}
this.currentLine = "";
break;
case 13: // carriage return
if (GUI.operating_system == "MacOS") {
text += "<br />";
}
this.currentLine = "";
break;
case 60:
text += '&lt';
break;
case 62:
text += '&gt';
break;
default:
text += String.fromCharCode(data[i]);
this.currentLine += String.fromCharCode(data[i]);
}
this.outputHistory += String.fromCharCode(data[i])
}
if (this.currentLine == 'Rebooting') {
CONFIGURATOR.cliActive = false;
CONFIGURATOR.cliValid = false;
GUI.log(i18n.getMessage('cliReboot'));
GUI.log(i18n.getMessage('deviceRebooting'));
if (BOARD.find_board_definition(CONFIG.boardIdentifier).vcp) { // VCP-based flight controls may crash old drivers, we catch and reconnect
$('a.connect').click();
GUI.timeout_add('start_connection',function start_connection() {
$('a.connect').click();
},2500);
} else {
GUI.timeout_add('waiting_for_bootup', function waiting_for_bootup() {
MSP.send_message(MSPCodes.MSP_STATUS, false, false, function() {
GUI.log(i18n.getMessage('deviceReady'));
if (!GUI.tab_switch_in_progress) {
$('#tabs ul.mode-connected .tab_setup a').click();
}
});
},1500); // 1500 ms seems to be just the right amount of delay to prevent data request timeouts
}
}
} else {
// try to catch part of valid CLI enter message // try to catch part of valid CLI enter message
this.validateText += String.fromCharCode(data[i]); validateText += currentChar;
text += String.fromCharCode(data[i]); cliOutput += currentChar;
continue;
} }
const escapeSequenceCode = 27;
const escapeSequenceCharLength = 3;
if (data[i] == escapeSequenceCode && !sequenceCharsToSkip) { // ESC + other
sequenceCharsToSkip = escapeSequenceCharLength;
}
if (sequenceCharsToSkip) {
sequenceCharsToSkip--;
continue;
}
const lineFeedCode = 10;
const carriageReturnCode = 13;
const backspaceCode = 8;
switch (data[i]) {
case lineFeedCode:
if (GUI.operating_system != "MacOS") {
cliOutput += "<br />";
}
this.cliBuffer = "";
break;
case carriageReturnCode:
if (GUI.operating_system == "MacOS") {
cliOutput += "<br />";
}
this.cliBuffer = "";
break;
case 60:
cliOutput += '&lt';
break;
case 62:
cliOutput += '&gt';
break;
case backspaceCode:
cliOutput = cliOutput.slice(0, -1);
this.cliBuffer = this.cliBuffer.slice(0, -1);
break;
default:
cliOutput += currentChar;
this.cliBuffer += currentChar;
}
this.outputHistory += currentChar;
if (this.cliBuffer == 'Rebooting') {
CONFIGURATOR.cliActive = false;
CONFIGURATOR.cliValid = false;
GUI.log(i18n.getMessage('cliReboot'));
GUI.log(i18n.getMessage('deviceRebooting'));
if (BOARD.find_board_definition(CONFIG.boardIdentifier).vcp) { // VCP-based flight controls may crash old drivers, we catch and reconnect
$('a.connect').click();
GUI.timeout_add('start_connection', function start_connection() {
$('a.connect').click();
}, 2500);
} else {
GUI.timeout_add('waiting_for_bootup', function waiting_for_bootup() {
MSP.send_message(MSPCodes.MSP_STATUS, false, false, function () {
GUI.log(i18n.getMessage('deviceReady'));
if (!GUI.tab_switch_in_progress) {
$('#tabs ul.mode-connected .tab_setup a').click();
}
});
}, 1500); // 1500 ms seems to be just the right amount of delay to prevent data request timeouts
}
}
} }
if (!CONFIGURATOR.cliValid && this.validateText.indexOf('CLI') != -1) { if (!CONFIGURATOR.cliValid && validateText.indexOf('CLI') !== -1) {
GUI.log(i18n.getMessage('cliEnter')); GUI.log(i18n.getMessage('cliEnter'));
CONFIGURATOR.cliValid = true; CONFIGURATOR.cliValid = true;
this.validateText = ""; validateText = "";
} }
$('.tab-cli .window .wrapper').append(text); writeToOutput(cliOutput);
$('.tab-cli .window').scrollTop($('.tab-cli .window .wrapper').height()); writeToPrompt(removePromptHash(this.cliBuffer));
}; };
TABS.cli.sendLine = function (line, callback) { TABS.cli.sendLine = function (line, callback) {
var bufferOut = new ArrayBuffer(line.length + 1); TABS.cli.send(line + '\n', callback);
};
TABS.cli.sendAutoComplete = function (line, callback) {
TABS.cli.send(line + '\t', callback);
};
TABS.cli.send = function (line, callback) {
var bufferOut = new ArrayBuffer(line.length);
var bufView = new Uint8Array(bufferOut); var bufView = new Uint8Array(bufferOut);
for (var c_key = 0; c_key < line.length; c_key++) { for (var c_key = 0; c_key < line.length; c_key++) {
bufView[c_key] = line.charCodeAt(c_key); bufView[c_key] = line.charCodeAt(c_key);
} }
bufView[line.length] = 0x0D; // enter (\n)
serial.send(bufferOut, callback); serial.send(bufferOut, callback);
} };
TABS.cli.cleanup = function (callback) { TABS.cli.cleanup = function (callback) {
if (!(CONFIGURATOR.connectionValid && CONFIGURATOR.cliValid && CONFIGURATOR.cliActive)) { if (!(CONFIGURATOR.connectionValid && CONFIGURATOR.cliValid && CONFIGURATOR.cliActive)) {

18
src/test/karma.conf.js Normal file
View file

@ -0,0 +1,18 @@
module.exports = function(config) {
config.set({
basePath: '../../',
frameworks: ['mocha', 'chai', 'sinon-chai'],
files: [
'./libraries/jquery-2.1.4.min.js',
'./libraries/bluebird.min.js',
'./src/js/serial.js',
'./src/js/data_storage.js',
'./src/js/localization.js',
'./src/js/gui.js',
'./src/js/tabs/cli.js',
'./src/test/**/*.js'
],
browsers: ['ChromeHeadless'],
singleRun: true
});
};

221
src/test/tabs/cli.js Normal file
View file

@ -0,0 +1,221 @@
describe('TABS.cli', () => {
function toArrayBuffer(string) {
var bufferOut = new ArrayBuffer(string.length);
var bufView = new Uint8Array(bufferOut);
for (var i = 0; i < string.length; i++) {
bufView[i] = string.charCodeAt(i);
}
return bufferOut;
}
describe('output', () => {
const cliTab = $('<div>').addClass('tab-cli');
const cliOutput = $('<div>').addClass('wrapper')
const cliPrompt = $('<textarea>');
cliTab.append($('<div>').addClass('window').append(cliOutput));
cliTab.append(cliPrompt);
before(() => {
$('body')
.append(cliTab);
CONFIGURATOR.cliValid = true;
});
after(() => cliTab.remove());
beforeEach(() => {
cliOutput.empty();
cliPrompt.val('');
TABS.cli.cliBuffer = "";
});
it('ambiguous auto-complete results', () => {
TABS.cli.read({
data: toArrayBuffer('\r\033[Kserialpassthrough\tservo\r\n# ser')
});
expect(cliOutput.html()).to.equal('<br>serialpassthrough\tservo<br># ser');
expect(cliPrompt.val()).to.equal('ser');
});
it('unambiguous auto-complete result', () => {
TABS.cli.read({
data: toArrayBuffer('serialpassthrough\r\n# serialpassthrough')
});
expect(cliOutput.html()).to.equal('serialpassthrough<br># serialpassthrough');
expect(cliPrompt.val()).to.equal('serialpassthrough');
});
it("escape characters (i.e. \033[K) are skipped", () => {
TABS.cli.read({
data: toArrayBuffer('\033[K')
});
expect(cliOutput.html()).to.equal('');
expect(cliPrompt.val()).to.equal('');
});
});
function triggerEnterKey(input) {
const enterKeycode = 13;
const event = $.Event("keypress");
event.which = enterKeycode;
input.trigger(event);
}
function triggerTabKey(input) {
const tabKeycode = 9;
const event = $.Event("keydown");
event.which = tabKeycode;
input.trigger(event);
}
describe('input', () => {
const content = $('<div>').attr('id', 'content');
const cliTab = $('<div>').addClass('tab-cli');
const cliPrompt = $('<textarea>');
cliTab.append(cliPrompt);
beforeEach(() => {
$('body')
.append(content);
// Stub loading of template.
sinon.stub($.fn, 'load').callsFake((file, callback) => {
content.append(cliTab);
callback();
});
sinon.stub(TABS.cli, 'send');
sinon.stub(Promise, 'reduce').callsFake((items, cb, initialValue) => {
items.forEach((line, idx) => cb(0, line, idx));
});
sinon.stub(window, 'Promise').callsFake(resolve => resolve(0));
sinon.stub(GUI, 'timeout_add').withArgs('CLI_send_slowly')
.callsFake((name, cb) => {
cb();
});
TABS.cli.cliBuffer = "";
});
afterEach(() => {
content.remove();
$.fn.load.restore();
TABS.cli.send.restore();
Promise.reduce.restore();
Promise.restore();
GUI.timeout_add.restore();
});
beforeEach(() => {
cliPrompt.val('');
content.empty();
});
it('tab key triggers serial message with appended tab char', done => {
TABS.cli.initialize(() => {
cliPrompt.val('serial');
triggerTabKey(cliPrompt);
expect(TABS.cli.send).to.have.been.calledOnce;
expect(TABS.cli.send).to.have.been.calledWith('serial\t');
done()
});
});
it('second auto complete in row', done => {
TABS.cli.cliBuffer = '# ser';
TABS.cli.initialize(() => {
cliPrompt.val('seri');
triggerTabKey(cliPrompt);
expect(TABS.cli.send).to.have.been.calledOnce;
expect(TABS.cli.send).to.have.been.calledWith('i\t');
done();
});
});
it('auto-complete command with trailing space', done => {
TABS.cli.cliBuffer = '# get ';
TABS.cli.initialize(() => {
cliPrompt.val('get r');
triggerTabKey(cliPrompt);
expect(TABS.cli.send).to.have.been.calledOnce;
expect(TABS.cli.send).to.have.been.calledWith('r\t');
done();
});
});
it('auto-complete after delete characters', done => {
TABS.cli.cliBuffer = '# serial';
TABS.cli.initialize(() => {
cliPrompt.val('ser');
triggerTabKey(cliPrompt);
const backspace = String.fromCharCode(127);
expect(TABS.cli.send).to.have.been.calledOnce;
expect(TABS.cli.send).to.have.been.calledWith(backspace.repeat(3) + '\t');
done();
});
});
it('enter after autocomplete', done => {
TABS.cli.cliBuffer = '# servo';
TABS.cli.initialize(() => {
cliPrompt.val('servo');
triggerEnterKey(cliPrompt);
expect(TABS.cli.send).to.have.been.calledOnce;
expect(TABS.cli.send).to.have.been.calledWith('\n');
done();
});
});
it('enter after autocomplete', done => {
TABS.cli.cliBuffer = '# ser';
TABS.cli.initialize(() => {
cliPrompt.val('servo');
triggerEnterKey(cliPrompt);
expect(TABS.cli.send).to.have.been.calledOnce;
expect(TABS.cli.send).to.have.been.calledWith('vo\n');
done();
});
});
it('enter after deleting characters', done => {
TABS.cli.cliBuffer = '# serial';
TABS.cli.initialize(() => {
cliPrompt.val('ser');
triggerEnterKey(cliPrompt);
const backspace = String.fromCharCode(127);
expect(TABS.cli.send).to.have.been.calledOnce;
expect(TABS.cli.send).to.have.been.calledWith(backspace.repeat(3) + '\n');
done();
});
});
});
});