Skip to content

Commit

Permalink
feat(stdin): add stdin history adn optimize rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Mollweide committed Apr 28, 2018
1 parent 8b71a4d commit 8c2fd87
Show file tree
Hide file tree
Showing 20 changed files with 478 additions and 385 deletions.
241 changes: 128 additions & 113 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,21 @@
"coveralls": "3.0.0",
"cross-env": "5.1.4",
"cz-conventional-changelog": "2.1.0",
"deep-copy": "1.4.2",
"eslint": "4.19.1",
"eslint-plugin-import": "2.11.0",
"husky": "0.14.3",
"istanbul": "0.4.5",
"jest": "22.4.3",
"jest-cli": "22.4.3",
"lint-staged": "7.0.4",
"lint-staged": "7.0.5",
"npm-run-all": "4.1.2",
"prettier": "1.12.1",
"rimraf": "2.6.2",
"semantic-release": "15.1.7"
},
"dependencies": {
"chalk": "2.4.0",
"chalk": "2.4.1",
"commander": "2.15.1",
"cross-spawn": "6.0.5",
"keypress": "0.2.1",
Expand All @@ -110,13 +111,14 @@
"precommit": "lint-staged && npm-run-all lint test check-coverage",
"prebuild": "rimraf dist",
"build": "node_modules/.bin/babel --out-dir dist --ignore *.spec.js src",
"start": "node_modules/.bin/babel --watch --out-dir dist --ignore *.spec.js src",
"prepack": "rimraf lerna-terminal-0.0.0-semantically-released.tgz package",
"pack": "npm pack && open lerna-terminal-0.0.0-semantically-released.tgz",
"check-coverage": "node_modules/.bin/istanbul check-coverage --statements 99 --branches 99 --functions 99 --lines 99",
"report-coverage": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls",
"pretest": "rimraf coverage",
"test": "cross-env NODE_ENV=test node_modules/.bin/jest --coverage",
"test:dev": "cross-env NODE_ENV=test node_modules/.bin/jest --watch --notify",
"test:dev": "cross-env NODE_ENV=test node_modules/.bin/jest --watch --coverage",
"test:single": "cross-env NODE_ENV=test node_modules/.bin/jest",
"lint": "npm-run-all lint:*",
"lint:js": "node_modules/.bin/eslint .",
Expand Down
48 changes: 42 additions & 6 deletions src/commandListener/index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,59 @@
'use strict';
const keypress = require('keypress');
const getFilledArray = require('../getFilledArray');
const { getUiState } = require('../store');

keypress(process.stdin);

let buffer = '';
const history = [];
let currentSelectedHistory = 0;

/**
* @param {Function<string>} onCommandEntered - the callback function
* @returns {void}
**/
function commandListener(onCommandEntered) {
const uiState = getUiState();

process.stdin.setEncoding('utf8');
process.stdin.setRawMode(true);

process.stdin.on('keypress', (letter, key) => {
if (key && key.ctrl && key.name === 'c') {
buffer = '';
process.exit();
} else if (key && key.name === 'backspace') {
process.stdout.write(getFilledArray(buffer.length, '\b').join(''));
buffer = buffer.substring(0, buffer.length - 1);
process.stdout.write(buffer);
uiState.onChange(buffer);
} else if (key && key.name === 'up') {
if (history[currentSelectedHistory]) {
buffer = history[currentSelectedHistory];
uiState.onChange(buffer);
} else {
buffer = '';
uiState.onChange(buffer);
}

if (currentSelectedHistory >= history.length) {
currentSelectedHistory = 0;
} else {
currentSelectedHistory += 1;
}
} else if (key && key.name === 'down') {
if (currentSelectedHistory >= history.length - 1) {
currentSelectedHistory = history.length - 2;
} else if (currentSelectedHistory <= -1) {
currentSelectedHistory = -1;
} else {
currentSelectedHistory -= 1;
}

if (history[currentSelectedHistory]) {
buffer = history[currentSelectedHistory];
uiState.onChange(buffer);
} else {
buffer = '';
uiState.onChange(buffer);
}
} else if (key && key.name === 'return') {
if (buffer.length > 0) {
onCommandEntered(
Expand All @@ -30,12 +62,16 @@ function commandListener(onCommandEntered) {
.replace(/\n/g, '')
.replace(/\t/g, '')
);
history.reverse();
history.push(buffer);
history.reverse();
}
buffer = '';
currentSelectedHistory = 0;
uiState.onChange('');
} else if (letter) {
process.stdout.write(getFilledArray(buffer.length, '\b').join(''));
buffer += letter;
process.stdout.write(buffer);
uiState.onChange(buffer);
}
});
process.stdin.resume();
Expand Down
212 changes: 161 additions & 51 deletions src/commandListener/index.spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
/* global jest, afterEach */
/* eslint global-require: 0*/
const commandListener = require('./index');
const { getUiState } = require('../store');

jest.mock('../store');

const _process = {
env: global.process.env,
stdin: {
setEncoding: jest.fn(),
setRawMode: jest.fn(),
Expand All @@ -17,103 +21,209 @@ const _process = {
},
};

const addString = inValue => {
global.process = Object.assign(_process, {
stdin: Object.assign(_process.stdin, {
on: (value, cb) => {
cb(inValue);
},
}),
});
};
const addString = inValue => [inValue];

const addReturn = () => [
'',
{
ctrl: false,
name: 'return',
},
];

const addCtrlC = () => [
'c',
{
ctrl: true,
name: 'c',
},
];

const addBackspace = () => [
'',
{
ctrl: false,
name: 'backspace',
},
];

const addUp = () => [
'',
{
name: 'up',
},
];
const addDown = () => [
'',
{
name: 'down',
},
];

describe('commandListener', () => {
beforeEach(() => {
getUiState.mockClear();
});
it('write result in buffer', done => {
addString('test');
getUiState.mockImplementation(() => ({
onChange(value) {
expect(value).toBe('test');
},
}));
global.process = Object.assign(_process, {
stdout: Object.assign(_process.stdout, {
write: value => {
if (value !== '') {
expect(value).toBe('test');
done();
}
stdin: Object.assign(_process.stdin, {
on: (value, cb) => {
cb(...addString('test'));
done();
},
}),
});
expect(commandListener(() => {})).toBe(undefined);
});
it('send command by press enter', done => {
getUiState.mockImplementation(() => ({
onChange(value) {
expect(value).toBe('');
},
}));
global.process = Object.assign(_process, {
stdin: Object.assign(_process.stdin, {
on: (value, cb) => {
expect(value).toBe('keypress');
cb('', {
ctrl: false,
name: 'return',
});
cb(...addReturn());
done();
},
}),
});
expect(
commandListener(value => {
expect(value).toBe('test');
done();
})
).toBe(undefined);
commandListener(value => {
expect(value).toBe('test');
done();
});
});
it('press enter but without anything entered', () => {
getUiState.mockImplementation(() => ({
onChange() {},
}));
expect(commandListener(() => {})).toBe(undefined);
});
it('press ctrl c', done => {
addString('test');
getUiState.mockImplementation(() => ({
onChange() {},
}));
global.process = Object.assign(_process, {
stdin: Object.assign(_process.stdin, {
on: (value, cb) => {
expect(value).toBe('keypress');
cb('c', {
ctrl: true,
name: 'c',
});
cb(...addString('test'));
cb(...addCtrlC());
},
}),
});
global.process = Object.assign(_process, {
exit: () => {
done();
},
});
expect(commandListener(() => {})).toBe(undefined);
});
it('write result in buffer', () => {
addString('test');
it('press backspace', done => {
let counter = 0;
getUiState.mockImplementation(() => ({
onChange(value) {
counter += 1;
if (counter === 2) {
expect(value).toBe('tes');
}
},
}));
global.process = Object.assign(_process, {
stdin: Object.assign(_process.stdin, {
on: (value, cb) => {
cb(...addString('test'));
cb(...addBackspace());
done();
},
}),
});
expect(commandListener(() => {})).toBe(undefined);
});
it('press backspace', done => {
addString('test');
it('press undefined key', () => {
getUiState.mockImplementation(() => ({
onChange() {},
}));
global.process = Object.assign(_process, {
stdin: Object.assign(_process.stdin, {
on: (value, cb) => {
cb('', {
ctrl: false,
name: 'backspace',
});
cb(...addString(undefined));
},
}),
});
expect(commandListener(() => {})).toBe(undefined);
});
it('go history up', done => {
let counter = 0;
const test = [
{ cmd: addReturn(), expected: '' },
{ cmd: addString('test'), expected: 'test' },
{ cmd: addReturn(), expected: '' },
{ cmd: addUp(), expected: 'test' },
{ cmd: addString('2'), expected: 'test2' },
{ cmd: addReturn(), expected: '' },
// history = [ 'test2', 'test', 'tes', 'test' ]
{ cmd: addUp(), expected: 'test2' },
{ cmd: addUp(), expected: 'test' },
{ cmd: addUp(), expected: 'tes' },
{ cmd: addUp(), expected: 'test' },
{ cmd: addUp(), expected: '' },
{ cmd: addUp(), expected: 'test2' },
{ cmd: addUp(), expected: 'test' },
];
getUiState.mockImplementation(() => ({
onChange(value) {
expect(value).toBe(test[counter].expected);
if (counter >= test.length - 1) {
done();
}
counter += 1;
},
}));
global.process = Object.assign(_process, {
stdout: Object.assign(_process.stdout, {
write: value => {
if (value[0] === 't') {
expect(value).toBe('tes');
done();
}
stdin: Object.assign(_process.stdin, {
on: (value, cb) => {
test.forEach(({ cmd }) => {
cb(...cmd);
});
},
}),
});
expect(commandListener(() => {})).toBe(undefined);
});
it('press undefined key', () => {
addString(undefined);
it('go history down', done => {
let counter = 0;
const test = [
{ cmd: addUp(), expected: 'tes' },
{ cmd: addUp(), expected: 'test' },
{ cmd: addDown(), expected: 'tes' },
{ cmd: addDown(), expected: 'test' },
{ cmd: addDown(), expected: 'test2' },
{ cmd: addDown(), expected: '' },
{ cmd: addDown(), expected: '' },
{ cmd: addDown(), expected: '' },
];
getUiState.mockImplementation(() => ({
onChange(value) {
expect(value).toBe(test[counter].expected);
if (counter >= test.length - 1) {
done();
}
counter += 1;
},
}));
global.process = Object.assign(_process, {
stdin: Object.assign(_process.stdin, {
on: (value, cb) => {
test.forEach(({ cmd }) => {
cb(...cmd);
});
},
}),
});
expect(commandListener(() => {})).toBe(undefined);
});
});
Loading

0 comments on commit 8c2fd87

Please sign in to comment.