2016-08-30 21:02:55 +00:00
|
|
|
|
import { Component, OnInit, Input, Output, EventEmitter, SimpleChange } from '@angular/core';
|
2016-08-29 21:12:31 +00:00
|
|
|
|
declare var Terminal: any;
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
selector: 'xterm-terminal',
|
|
|
|
|
templateUrl: 'app/xterm-terminal/xterm-terminal.component.html',
|
|
|
|
|
styleUrls: ['app/xterm-terminal/xterm-terminal.component.css']
|
|
|
|
|
})
|
|
|
|
|
export class XtermTerminalComponent implements OnInit {
|
2016-08-30 21:02:55 +00:00
|
|
|
|
@Input() response: string;
|
|
|
|
|
@Input() shell: string;
|
|
|
|
|
@Output() command: EventEmitter<string> = new EventEmitter<string>();
|
2016-09-06 14:32:24 +00:00
|
|
|
|
|
|
|
|
|
// handle up/down arrow functionality
|
2016-08-30 21:02:55 +00:00
|
|
|
|
previousCommands: any = [];
|
2016-09-02 19:13:07 +00:00
|
|
|
|
poppedCommands: any = [];
|
2016-09-06 14:32:24 +00:00
|
|
|
|
last: number;
|
|
|
|
|
|
|
|
|
|
// handle user input
|
|
|
|
|
buffer: string = ''; // the buffer is our string of user input
|
|
|
|
|
blockCmd: string = ''; // handle describe and control blocks
|
2016-08-30 21:02:55 +00:00
|
|
|
|
|
|
|
|
|
// xterm variables
|
2016-08-29 21:12:31 +00:00
|
|
|
|
terminalContainer: any;
|
|
|
|
|
term: any;
|
|
|
|
|
optionElements: any;
|
2016-08-30 21:02:55 +00:00
|
|
|
|
cols: string;
|
|
|
|
|
rows: string;
|
2016-08-29 21:12:31 +00:00
|
|
|
|
|
2016-09-06 14:32:24 +00:00
|
|
|
|
ngOnInit() {
|
|
|
|
|
// set up Xterm terminal (https://github.com/sourcelair/xterm.js)
|
2016-09-02 19:13:07 +00:00
|
|
|
|
this.terminalContainer = document.getElementById('terminal-container');
|
|
|
|
|
this.cols = '70';
|
2016-08-29 21:12:31 +00:00
|
|
|
|
this.rows = '70';
|
|
|
|
|
this.createTerminal();
|
2016-09-06 14:32:24 +00:00
|
|
|
|
}
|
2016-08-29 21:12:31 +00:00
|
|
|
|
|
2016-09-06 14:32:24 +00:00
|
|
|
|
// watch for changes on the value of response and call printResponse
|
2016-08-30 21:02:55 +00:00
|
|
|
|
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
|
|
|
|
|
if (changes['response'] && this.term) {
|
2016-09-06 14:32:24 +00:00
|
|
|
|
let currentResponse = changes['response'].currentValue;
|
|
|
|
|
this.printResponse(currentResponse);
|
2016-08-30 21:02:55 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-06 14:32:24 +00:00
|
|
|
|
// create Xterm terminal (https://github.com/sourcelair/xterm.js)
|
2016-08-29 21:12:31 +00:00
|
|
|
|
createTerminal() {
|
|
|
|
|
while (this.terminalContainer.children.length) {
|
|
|
|
|
this.terminalContainer.removeChild(this.terminalContainer.children[0]);
|
|
|
|
|
}
|
|
|
|
|
this.term = new Terminal({
|
|
|
|
|
cursorBlink: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.term.open(this.terminalContainer);
|
|
|
|
|
this.term.fit();
|
|
|
|
|
|
|
|
|
|
var initialGeometry = this.term.proposeGeometry(),
|
|
|
|
|
cols = initialGeometry.cols,
|
|
|
|
|
rows = initialGeometry.rows;
|
|
|
|
|
|
|
|
|
|
this.cols = cols;
|
|
|
|
|
this.rows = rows;
|
|
|
|
|
this.runFakeTerminal()
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-06 14:32:24 +00:00
|
|
|
|
// more Xterm terminal stuff. we're faking it. faking it can be a good thing ;)
|
2016-08-29 21:12:31 +00:00
|
|
|
|
runFakeTerminal() {
|
|
|
|
|
if (this.term._initialized) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.term._initialized = true;
|
|
|
|
|
this.setPrompt();
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-06 14:32:24 +00:00
|
|
|
|
// if the response matches the special message we send when the user
|
|
|
|
|
// has entered 'next' or 'prev' (to navigate through the demo)
|
|
|
|
|
// then no need to print the response, just set the prompt. otherwise,
|
|
|
|
|
// print response and set prompt
|
|
|
|
|
printResponse(response) {
|
|
|
|
|
if (response.match(/.30mnext|.30mprev/)) {
|
2016-09-13 11:08:13 +00:00
|
|
|
|
// call createTerminal to clear terminal screen on next and prev
|
|
|
|
|
this.createTerminal();
|
2016-09-06 14:32:24 +00:00
|
|
|
|
} else {
|
|
|
|
|
this.term.writeln(response);
|
|
|
|
|
this.setPrompt();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// the value of shell is taken as an input. the default
|
|
|
|
|
// shellprompt is displayed unless shell is set to 'inspec-shell'
|
2016-08-29 21:12:31 +00:00
|
|
|
|
setPrompt() {
|
2016-08-30 21:02:55 +00:00
|
|
|
|
this.buffer = '';
|
|
|
|
|
if (this.shell === 'inspec-shell') {
|
2016-09-06 14:32:24 +00:00
|
|
|
|
if (this.blockCmd != '') {
|
|
|
|
|
this.term.write(' [32minspec>*'); // green inspec shell prompt with * to denote multi-line command
|
|
|
|
|
} else {
|
|
|
|
|
this.term.write(' [32minspec> '); // green inspec shell prompt
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2016-09-13 11:08:13 +00:00
|
|
|
|
this.term.write('[36m$ '); // blue regular shell prompt
|
2016-09-06 14:32:24 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// delete everything on the line
|
|
|
|
|
deleteCharacters() {
|
|
|
|
|
// don't delete the prompt
|
2016-09-13 11:08:13 +00:00
|
|
|
|
let letters = this.term.x - 2;
|
2016-09-06 14:32:24 +00:00
|
|
|
|
for (var i = 0; i < letters; i++) {
|
|
|
|
|
this.term.write('\b \b');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// keydown calls handleDelete to check if the user is holding down the backpace key
|
|
|
|
|
// TODO: make this work! the backspace event isn't coming in for some reason :(
|
|
|
|
|
handleDelete(ev) {
|
|
|
|
|
if (ev.keyCode == 8) {
|
|
|
|
|
this.deleteCharacters();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handle describe and control blocks. if the command entered matches the
|
|
|
|
|
// syntax we would generally expect in a control/describe block, then we
|
|
|
|
|
// wait to have the whole string command (as marked by 'end' for a simple describe
|
|
|
|
|
// block and end end for a control block) and emit that
|
|
|
|
|
handleBlockCommand(buffer) {
|
|
|
|
|
this.blockCmd += buffer + '';
|
|
|
|
|
if (this.blockCmd.match(/^control/)) {
|
|
|
|
|
if (this.blockCmd.match(/end.*end.*/)) {
|
|
|
|
|
this.command.emit(this.blockCmd);
|
|
|
|
|
this.blockCmd = '';
|
|
|
|
|
} else {
|
|
|
|
|
this.setPrompt();
|
|
|
|
|
}
|
2016-08-30 21:02:55 +00:00
|
|
|
|
} else {
|
2016-09-06 14:32:24 +00:00
|
|
|
|
if (buffer.match(/^end$/)) {
|
|
|
|
|
this.command.emit(this.blockCmd);
|
|
|
|
|
this.blockCmd = '';
|
|
|
|
|
} else {
|
|
|
|
|
this.setPrompt();
|
|
|
|
|
}
|
2016-08-30 21:02:55 +00:00
|
|
|
|
}
|
2016-08-29 21:12:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-06 14:32:24 +00:00
|
|
|
|
// onKey will check if the character is printable (on keyup) and add it to the buffer. if the enter key is hit
|
|
|
|
|
// the value of the buffer is emitted as 'command'. onKey also supports proper backspace handling and
|
|
|
|
|
// the ability to up-arrow through previous commands/down arrow back through them.
|
2016-08-29 21:12:31 +00:00
|
|
|
|
onKey(ev) {
|
|
|
|
|
var shell = null
|
2016-09-06 14:32:24 +00:00
|
|
|
|
// determine if the character is a printable one
|
2016-09-02 19:13:07 +00:00
|
|
|
|
var printable = ['Alt', 'Control', 'Meta', 'Shift', 'CapsLock', 'Tab', 'Escape', 'ArrowLeft', 'ArrowRight'].indexOf(ev.key) == -1
|
2016-08-29 21:12:31 +00:00
|
|
|
|
|
2016-08-30 21:02:55 +00:00
|
|
|
|
// on enter, save command to array and send current value of buffer
|
2016-09-06 14:32:24 +00:00
|
|
|
|
// to parent component (app)
|
2016-08-29 21:12:31 +00:00
|
|
|
|
if (ev.keyCode == 13) {
|
2016-09-13 11:08:13 +00:00
|
|
|
|
if ((this.buffer === 'clear') || (this.buffer === 'cls')) {
|
2016-09-06 14:32:24 +00:00
|
|
|
|
this.createTerminal();
|
|
|
|
|
} else {
|
|
|
|
|
this.previousCommands.push(this.buffer);
|
|
|
|
|
this.term.write('\r\n');
|
|
|
|
|
if (this.shell === 'inspec-shell') {
|
|
|
|
|
// if the command entered matches any of the typical describe and
|
|
|
|
|
// control blocks, then we call handleBlockCommand to not emit the
|
|
|
|
|
// value until we have the whole multi-line command
|
|
|
|
|
if (this.buffer.match(/^describe.*|^it.*|^end$|^impact.*|^title.*|^control.*/)) {
|
|
|
|
|
this.handleBlockCommand(this.buffer);
|
2016-09-13 11:08:13 +00:00
|
|
|
|
} else {this.command.emit(this.buffer);}
|
2016-09-06 14:32:24 +00:00
|
|
|
|
} else {
|
|
|
|
|
this.command.emit(this.buffer);
|
2016-09-01 15:19:04 +00:00
|
|
|
|
}
|
2016-08-29 21:12:31 +00:00
|
|
|
|
}
|
2016-08-30 21:02:55 +00:00
|
|
|
|
}
|
2016-08-29 21:12:31 +00:00
|
|
|
|
// on backspace, pop characters from buffer
|
2016-08-30 21:02:55 +00:00
|
|
|
|
else if (ev.keyCode == 8) {
|
2016-09-06 14:32:24 +00:00
|
|
|
|
// if inspec shell is being used, this needs to be set to 9 to account for the extra letters
|
|
|
|
|
if (this.shell === 'inspec-shell') {
|
2016-09-13 11:08:13 +00:00
|
|
|
|
if (this.term.x > 8) {
|
2016-09-06 14:32:24 +00:00
|
|
|
|
this.buffer = this.buffer.substr(0, this.buffer.length-1);
|
|
|
|
|
this.term.write('\b \b');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// setting the value here to 3 ensures that we don't delete the promp '$' or the space after it
|
2016-09-13 11:08:13 +00:00
|
|
|
|
if (this.term.x > 2) {
|
2016-09-06 14:32:24 +00:00
|
|
|
|
this.buffer = this.buffer.substr(0, this.buffer.length-1);
|
|
|
|
|
this.term.write('\b \b');
|
|
|
|
|
}
|
2016-08-29 21:12:31 +00:00
|
|
|
|
}
|
2016-08-30 21:02:55 +00:00
|
|
|
|
}
|
2016-09-06 14:32:24 +00:00
|
|
|
|
// on up arrow, delete anything on line, print previous command
|
2016-09-02 19:13:07 +00:00
|
|
|
|
// and push last to poppedCommands
|
2016-08-30 21:02:55 +00:00
|
|
|
|
else if (ev.keyCode === 38) {
|
|
|
|
|
let last;
|
2016-09-06 14:32:24 +00:00
|
|
|
|
this.buffer = '';
|
2016-08-30 21:02:55 +00:00
|
|
|
|
if (this.previousCommands.length > 0) {
|
|
|
|
|
last = this.previousCommands.pop();
|
2016-09-02 19:13:07 +00:00
|
|
|
|
this.poppedCommands.push(last);
|
2016-08-30 21:02:55 +00:00
|
|
|
|
} else {
|
|
|
|
|
last = '';
|
|
|
|
|
}
|
2016-09-06 14:32:24 +00:00
|
|
|
|
this.deleteCharacters();
|
|
|
|
|
this.buffer = last;
|
2016-08-30 21:02:55 +00:00
|
|
|
|
this.term.write(last);
|
|
|
|
|
}
|
2016-09-06 14:32:24 +00:00
|
|
|
|
// on down arrow, delete anything on line, print last item from poppedCommands
|
2016-09-02 19:13:07 +00:00
|
|
|
|
// and push previous to previousCommands
|
|
|
|
|
else if (ev.keyCode === 40) {
|
|
|
|
|
let previous;
|
2016-09-06 14:32:24 +00:00
|
|
|
|
this.buffer = '';
|
2016-09-02 19:13:07 +00:00
|
|
|
|
if (this.poppedCommands.length > 0) {
|
|
|
|
|
previous = this.poppedCommands.pop();
|
|
|
|
|
this.previousCommands.push(previous);
|
|
|
|
|
} else {
|
|
|
|
|
previous = '';
|
|
|
|
|
}
|
2016-09-06 14:32:24 +00:00
|
|
|
|
this.deleteCharacters();
|
|
|
|
|
this.buffer = previous;
|
2016-09-02 19:13:07 +00:00
|
|
|
|
this.term.write(previous);
|
|
|
|
|
}
|
2016-09-06 14:32:24 +00:00
|
|
|
|
// if the character is printable and none of the event key codes from above were matched
|
|
|
|
|
// then we write the character on the prompt line and add the character to the buffer
|
2016-08-30 21:02:55 +00:00
|
|
|
|
else if (printable) {
|
2016-08-29 21:12:31 +00:00
|
|
|
|
this.term.write(ev.key);
|
|
|
|
|
this.buffer += ev.key;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|