diff --git a/ui/app/utils/classes/exec-command-editor-xterm-adapter.js b/ui/app/utils/classes/exec-command-editor-xterm-adapter.js index 6a2e96f72..45212f4f1 100644 --- a/ui/app/utils/classes/exec-command-editor-xterm-adapter.js +++ b/ui/app/utils/classes/exec-command-editor-xterm-adapter.js @@ -1,6 +1,11 @@ +import KEYS from 'nomad-ui/utils/keys'; + const REVERSE_WRAPAROUND_MODE = '\x1b[?45h'; const BACKSPACE_ONE_CHARACTER = '\x08 \x08'; +// eslint-disable-next-line no-control-regex +const UNPRINTABLE_CHARACTERS_REGEX = /[\x00-\x1F]/g; + export default class ExecCommandEditorXtermAdapter { constructor(terminal, setCommandCallback, command) { this.terminal = terminal; @@ -8,34 +13,44 @@ export default class ExecCommandEditorXtermAdapter { this.command = command; - this.keyListener = terminal.onKey(e => { - this.handleKeyEvent(e); + this.dataListener = terminal.onData(data => { + this.handleDataEvent(data); }); // Allows tests to bypass synthetic keyboard event restrictions - terminal.simulateCommandKeyEvent = this.handleKeyEvent.bind(this); + terminal.simulateCommandDataEvent = this.handleDataEvent.bind(this); terminal.write(REVERSE_WRAPAROUND_MODE); } - handleKeyEvent(e) { - // Issue to handle arrow keys etc: https://github.com/hashicorp/nomad/issues/7463 - if (e.domEvent.key === 'Enter') { + handleDataEvent(data) { + if ( + data === KEYS.LEFT_ARROW || + data === KEYS.UP_ARROW || + data === KEYS.RIGHT_ARROW || + data === KEYS.DOWN_ARROW + ) { + // Ignore arrow keys + } else if (data === KEYS.CONTROL_U) { + this.terminal.write(BACKSPACE_ONE_CHARACTER.repeat(this.command.length)); + this.command = ''; + } else if (data === KEYS.ENTER) { this.terminal.writeln(''); this.setCommandCallback(this.command); - this.keyListener.dispose(); - } else if (e.domEvent.key === 'Backspace') { + this.dataListener.dispose(); + } else if (data === KEYS.DELETE) { if (this.command.length > 0) { this.terminal.write(BACKSPACE_ONE_CHARACTER); this.command = this.command.slice(0, -1); } - } else if (e.key.length > 0) { - this.terminal.write(e.key); - this.command = `${this.command}${e.key}`; + } else if (data.length > 0) { + const strippedData = data.replace(UNPRINTABLE_CHARACTERS_REGEX, ''); + this.terminal.write(strippedData); + this.command = `${this.command}${strippedData}`; } } destroy() { - this.keyListener.dispose(); + this.dataListener.dispose(); } } diff --git a/ui/app/utils/keys.js b/ui/app/utils/keys.js new file mode 100644 index 000000000..2520b1633 --- /dev/null +++ b/ui/app/utils/keys.js @@ -0,0 +1,10 @@ +export default { + LEFT_ARROW: '\x1b[D', + UP_ARROW: '\x1b[A', + RIGHT_ARROW: '\x1b[C', + DOWN_ARROW: '\x1b[B', + CONTROL_A: '\x01', + CONTROL_U: '\x15', + ENTER: '\r', + DELETE: '\x7F', +}; diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js index d05020bf4..d5b7a7650 100644 --- a/ui/tests/acceptance/exec-test.js +++ b/ui/tests/acceptance/exec-test.js @@ -4,6 +4,7 @@ import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import Service from '@ember/service'; import Exec from 'nomad-ui/tests/pages/exec'; +import KEYS from 'nomad-ui/utils/keys'; module('Acceptance | exec', function(hooks) { setupApplicationTest(hooks); @@ -331,20 +332,20 @@ module('Acceptance | exec', function(hooks) { await settled(); // Delete /bash - await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); + await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); + await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); + await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); + await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); // Delete /bin and try to go beyond - await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); + await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); + await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); + await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); + await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); + await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); + await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); await settled(); @@ -356,9 +357,7 @@ module('Acceptance | exec', function(hooks) { `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]}` ); - await window.execTerminal.simulateCommandKeyEvent({ key: '/', domEvent: {} }); - await window.execTerminal.simulateCommandKeyEvent({ key: 's', domEvent: {} }); - await window.execTerminal.simulateCommandKeyEvent({ key: 'h', domEvent: {} }); + await window.execTerminal.simulateCommandDataEvent('/sh'); await Exec.terminal.pressEnter(); await settled(); diff --git a/ui/tests/integration/util/exec-command-editor-xterm-adapter-test.js b/ui/tests/integration/util/exec-command-editor-xterm-adapter-test.js index 20269855c..13c579f34 100644 --- a/ui/tests/integration/util/exec-command-editor-xterm-adapter-test.js +++ b/ui/tests/integration/util/exec-command-editor-xterm-adapter-test.js @@ -4,6 +4,7 @@ import { module, test } from 'qunit'; import { render, settled } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { Terminal } from 'xterm-vendor'; +import KEYS from 'nomad-ui/utils/keys'; module('Integration | Utility | exec-command-editor-xterm-adapter', function(hooks) { setupRenderingTest(hooks); @@ -29,14 +30,14 @@ module('Integration | Utility | exec-command-editor-xterm-adapter', function(hoo '/bin/long-command' ); - await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); - await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } }); + await terminal.simulateCommandDataEvent(KEYS.DELETE); + await terminal.simulateCommandDataEvent(KEYS.DELETE); + await terminal.simulateCommandDataEvent(KEYS.DELETE); + await terminal.simulateCommandDataEvent(KEYS.DELETE); + await terminal.simulateCommandDataEvent(KEYS.DELETE); + await terminal.simulateCommandDataEvent(KEYS.DELETE); + await terminal.simulateCommandDataEvent(KEYS.DELETE); + await terminal.simulateCommandDataEvent(KEYS.DELETE); await settled(); @@ -48,6 +49,89 @@ module('Integration | Utility | exec-command-editor-xterm-adapter', function(hoo '/bin/long' ); - await terminal.simulateCommandKeyEvent({ domEvent: { key: 'Enter' } }); + await terminal.simulateCommandDataEvent(KEYS.ENTER); + }); + + test('it ignores arrow keys and unprintable characters other than ^U', async function(assert) { + let done = assert.async(); + + await render(hbs` +
+ `); + + let terminal = new Terminal({ cols: 72 }); + terminal.open(document.getElementById('terminal')); + + terminal.write('/bin/bash'); + + new ExecCommandEditorXtermAdapter( + terminal, + command => { + assert.equal(command, '/bin/bash!'); + done(); + }, + '/bin/bash' + ); + + await terminal.simulateCommandDataEvent(KEYS.RIGHT_ARROW); + await terminal.simulateCommandDataEvent(KEYS.RIGHT_ARROW); + await terminal.simulateCommandDataEvent(KEYS.LEFT_ARROW); + await terminal.simulateCommandDataEvent(KEYS.UP_ARROW); + await terminal.simulateCommandDataEvent(KEYS.UP_ARROW); + await terminal.simulateCommandDataEvent(KEYS.DOWN_ARROW); + await terminal.simulateCommandDataEvent(KEYS.CONTROL_A); + await terminal.simulateCommandDataEvent('!'); + + await settled(); + + assert.equal(terminal.buffer.cursorY, 0); + assert.equal(terminal.buffer.cursorX, 10); + + assert.equal( + terminal.buffer + .getLine(0) + .translateToString() + .trim(), + '/bin/bash!' + ); + + await terminal.simulateCommandDataEvent(KEYS.ENTER); + }); + + test('it supports typing ^U to delete the entire command', async function(assert) { + let done = assert.async(); + + await render(hbs` + + `); + + let terminal = new Terminal({ cols: 10 }); + terminal.open(document.getElementById('terminal')); + + terminal.write('to-delete'); + + new ExecCommandEditorXtermAdapter( + terminal, + command => { + assert.equal(command, '!'); + done(); + }, + 'to-delete' + ); + + await terminal.simulateCommandDataEvent(KEYS.CONTROL_U); + + await settled(); + + assert.equal( + terminal.buffer + .getLine(0) + .translateToString() + .trim(), + '' + ); + + await terminal.simulateCommandDataEvent('!'); + await terminal.simulateCommandDataEvent(KEYS.ENTER); }); });