'use strict'; /*global chrome,GUI,TABS,nwdialog,$*/ TABS.cli = { lineDelayMs: 50, profileSwitchDelayMs: 100, outputHistory: "", cliBuffer: "", GUI: { snippetPreviewWindow: null, }, }; function removePromptHash(promptText) { return promptText.replace(/^# /, ''); } 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); } function copyToClipboard(text) { function onCopySuccessful() { const button = $('.tab-cli .copy'); const origText = button.text(); const origWidth = button.css("width"); button.text(chrome.i18n.getMessage("cliCopySuccessful")); button.css({ width: origWidth, textAlign: "center", }); setTimeout(() => { button.text(origText); button.css({ width: "", textAlign: "", }); }, 1500); } function onCopyFailed(ex) { console.warn(ex); } function nwCopy(text) { try { let clipboard = require('nw.gui').Clipboard.get(); clipboard.set(text, "text"); onCopySuccessful(); } catch (ex) { onCopyFailed(ex); } } function webCopy(text) { navigator.clipboard.writeText(text) .then(onCopySuccessful, onCopyFailed); } let copyFunc; try { let nwGui = require('nw.gui'); copyFunc = nwCopy; } catch (e) { copyFunc = webCopy; } copyFunc(text); } function sendLinesWithDelay(outputArray) { return (delay, line, index) => { return new Promise((resolve) => { helper.timeout.add('CLI_send_slowly', () => { let processingDelay = TABS.cli.lineDelayMs; if (line.toLowerCase().startsWith('profile')) { processingDelay = TABS.cli.profileSwitchDelayMs; } const isLastCommand = outputArray.length === index + 1; if (isLastCommand && TABS.cli.cliBuffer) { line = getCliCommand(line, TABS.cli.cliBuffer); } TABS.cli.sendLine(line, () => { resolve(processingDelay); }); }, delay); }); }; } TABS.cli.initialize = function (callback) { var self = this; if (GUI.active_tab != 'cli') { GUI.active_tab = 'cli'; googleAnalytics.sendAppView('CLI'); } // Flush MSP queue as well as all MSP registered callbacks helper.mspQueue.flush(); MSP.callbacks_cleanup(); self.outputHistory = ""; self.cliBuffer = ""; const clipboardCopySupport = (() => { let nwGui = null; try { nwGui = require('nw.gui'); } catch (e) {} return !(nwGui == null && !navigator.clipboard) })(); function executeCommands(out_string) { self.history.add(out_string.trim()); var outputArray = out_string.split("\n"); Promise.reduce(outputArray, sendLinesWithDelay(outputArray), 0); } GUI.load("./tabs/cli.html", function () { // translate to user-selected language localize(); CONFIGURATOR.cliActive = true; var textarea = $('.tab-cli textarea[name="commands"]'); $('.tab-cli .save').click(function() { var prefix = 'cli'; var suffix = 'txt'; var filename = generateFilename(prefix, suffix); var accepts = [{ description: suffix.toUpperCase() + ' files', extensions: [suffix], }]; nwdialog.setContext(document); nwdialog.saveFileDialog(filename, accepts, '', function(result) { if (!result) { GUI.log(chrome.i18n.getMessage('cliSaveToFileAborted')); return; } const fs = require('fs'); fs.writeFile(result, self.outputHistory, (err) => { if (err) { GUI.log('Error writing file'); return console.error(err); } GUI.log('File saved'); }); }); }); $('.tab-cli .clear').click(function() { self.outputHistory = ""; $('.tab-cli .window .wrapper').empty(); }); if (clipboardCopySupport) { $('.tab-cli .copy').click(function() { copyToClipboard(self.outputHistory); }); } else { $('.tab-cli .copy').hide(); } $('.tab-cli .load').click(function() { nwdialog.setContext(document); nwdialog.openFileDialog(".txt", false, '', function(result) { if (!result) { console.log('No file selected'); return; } let previewArea = $("#snippetpreviewcontent textarea#preview"); function executeSnippet() { const commands = previewArea.val(); executeCommands(commands); self.GUI.snippetPreviewWindow.close(); } function previewCommands(result) { if (!self.GUI.snippetPreviewWindow) { self.GUI.snippetPreviewWindow = new jBox("Modal", { id: "snippetPreviewWindow", width: 'auto', height: 'auto', closeButton: 'title', animation: false, isolateScroll: false, title: chrome.i18n.getMessage("cliConfirmSnippetDialogTitle"), content: $('#snippetpreviewcontent'), onCreated: () => $("#snippetpreviewcontent a.confirm").click(() => executeSnippet()), }); } previewArea.val(result); self.GUI.snippetPreviewWindow.open(); } const fs = require('fs'); fs.readFile(result, (err, data) => { if (err) { GUI.log('Error reading file'); return console.error(err); } previewCommands(data); }); }); }); // 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) { const enterKeyCode = 13; if (event.which == enterKeyCode) { event.preventDefault(); // prevent the adding of new line var out_string = textarea.val(); self.history.add(out_string.trim()); var outputArray = out_string.split("\n"); Promise.reduce(outputArray, sendLinesWithDelay(outputArray), 0); textarea.val(''); } }); textarea.keyup(function (event) { var keyUp = {38: true}, keyDown = {40: true}; if (event.keyCode in keyUp) { textarea.val(self.history.prev()); } if (event.keyCode in keyDown) { textarea.val(self.history.next()); } }); // give input element user focus textarea.focus(); helper.timeout.add('enter_cli', function enter_cli() { // Enter CLI mode var bufferOut = new ArrayBuffer(1); var bufView = new Uint8Array(bufferOut); bufView[0] = 0x23; // # serial.send(bufferOut); }, 250); GUI.content_ready(callback); }); }; TABS.cli.history = { history: [], index: 0 }; TABS.cli.history.add = function (str) { this.history.push(str); this.index = this.history.length; }; TABS.cli.history.prev = function () { if (this.index > 0) this.index -= 1; return this.history[this.index]; }; TABS.cli.history.next = function () { if (this.index < this.history.length) this.index += 1; return this.history[this.index - 1]; }; const backspaceCode = 8; const lineFeedCode = 10; const carriageReturnCode = 13; function writeToOutput(text) { $('.tab-cli .window .wrapper').append(text); $('.tab-cli .window').scrollTop($('.tab-cli .window .wrapper').height()); } function writeLineToOutput(text) { if (text.startsWith("### ERROR: ")) { writeToOutput('' + text + '
'); } else { writeToOutput(text + "
"); } } function setPrompt(text) { $('.tab-cli textarea').val(text); } TABS.cli.read = function (readInfo) { /* Some info about handling line feeds and carriage return line feed = LF = \n = 0x0A = 10 carriage return = CR = \r = 0x0D = 13 MAC only understands CR Linux and Unix only understand LF Windows understands (both) CRLF Chrome OS currently unknown */ var data = new Uint8Array(readInfo.data), validateText = "", sequenceCharsToSkip = 0; for (var i = 0; i < data.length; i++) { const currentChar = String.fromCharCode(data[i]); if (!CONFIGURATOR.cliValid) { // try to catch part of valid CLI enter message validateText += currentChar; writeToOutput(currentChar); continue; } const escapeSequenceCode = 27; const escapeSequenceCharLength = 3; if (data[i] == escapeSequenceCode && !sequenceCharsToSkip) { // ESC + other sequenceCharsToSkip = escapeSequenceCharLength; } if (sequenceCharsToSkip) { sequenceCharsToSkip--; continue; } switch (data[i]) { case lineFeedCode: if (GUI.operating_system === "Windows") { writeLineToOutput(this.cliBuffer); this.cliBuffer = ""; } break; case carriageReturnCode: if (GUI.operating_system !== "Windows") { writeLineToOutput(this.cliBuffer); this.cliBuffer = ""; } break; case 60: this.cliBuffer += '<'; break; case 62: this.cliBuffer += '>'; break; case backspaceCode: this.cliBuffer = this.cliBuffer.slice(0, -1); break; default: this.cliBuffer += currentChar; } this.outputHistory += currentChar; if (this.cliBuffer == 'Rebooting') { CONFIGURATOR.cliActive = false; CONFIGURATOR.cliValid = false; GUI.log(chrome.i18n.getMessage('cliReboot')); GUI.log(chrome.i18n.getMessage('deviceRebooting')); GUI.handleReconnect(); } } if (!CONFIGURATOR.cliValid && validateText.indexOf('CLI') !== -1) { GUI.log(chrome.i18n.getMessage('cliEnter')); CONFIGURATOR.cliValid = true; validateText = ""; } setPrompt(removePromptHash(this.cliBuffer)); }; TABS.cli.sendLine = function (line, callback) { this.send(line + '\n', callback); }; TABS.cli.sendAutoComplete = function (line, callback) { this.send(line + '\t', callback); }; TABS.cli.send = function (line, callback) { var bufferOut = new ArrayBuffer(line.length); var bufView = new Uint8Array(bufferOut); for (var c_key = 0; c_key < line.length; c_key++) { bufView[c_key] = line.charCodeAt(c_key); } serial.send(bufferOut, callback); }; TABS.cli.cleanup = function (callback) { if (!(CONFIGURATOR.connectionValid && CONFIGURATOR.cliValid && CONFIGURATOR.cliActive)) { if (callback) callback(); return; } this.send(getCliCommand('exit\r', this.cliBuffer), function (writeInfo) { // we could handle this "nicely", but this will do for now // (another approach is however much more complicated): // we can setup an interval asking for data lets say every 200ms, when data arrives, callback will be triggered and tab switched // we could probably implement this someday helper.timeout.add('waiting_for_bootup', function waiting_for_bootup() { if (callback) callback(); }, 1000); // if we dont allow enough time to reboot, CRC of "first" command sent will fail, keep an eye for this one CONFIGURATOR.cliActive = false; }); };