Skip to content

Commit

Permalink
Add useInput hook to handle user input
Browse files Browse the repository at this point in the history
  • Loading branch information
Vadim Demedes committed Sep 15, 2019
1 parent 92e2995 commit dd3efe8
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 131 deletions.
2 changes: 2 additions & 0 deletions examples/useinput/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
'use strict';
require('import-jsx')('./useinput');
41 changes: 41 additions & 0 deletions examples/useinput/useinput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';
const {useState, useContext} = require('react');
const React = require('react');
const {render, useInput, Box, AppContext} = require('../..');

const Robot = () => {
const {exit} = useContext(AppContext);
const [x, setX] = useState(1);
const [y, setY] = useState(1);

useInput((input, meta) => {
if (input === 'q') {
exit();
}

if (meta.left) {
setX(Math.max(1, x - 1));
}

if (meta.right) {
setX(Math.min(20, x + 1));
}

if (meta.up) {
setY(Math.max(1, y - 1));
}

if (meta.down) {
setY(Math.min(10, y + 1));
}
});

return (
<Box flexDirection="column">
<Box>Use arrow keys to move the face. q to exit</Box>
<Box height={12} paddingLeft={x} paddingTop={y}>^_^</Box>
</Box>
);
};

render(<Robot/>);
2 changes: 0 additions & 2 deletions examples/usekeypress/index.js

This file was deleted.

42 changes: 0 additions & 42 deletions examples/usekeypress/usekeypress.js

This file was deleted.

16 changes: 11 additions & 5 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as React from "react";
import { Key } from 'readline';

export interface RenderOptions {
/**
Expand Down Expand Up @@ -46,13 +45,20 @@ export type Instance = {
export type Unmount = () => void;

/**
* Hook that calls keyPressHandler callback with whatever key has been pressed.
* See key to handle modifiers correctly.
* Hook that calls inputHandler callback with input that program received.
* Additionally contains helpful metadata for detecting when arrow keys were pressed.
*/
export function useKeypress(
keyPressHandler: (str?: string, key?: Key) => void
export function useInput(
inputHandler: (input: string, meta: Meta) => void
): void;

export interface Meta {
up: boolean
down: boolean
left: boolean
right: boolean
};

/**
* Mount a component and render the output.
*/
Expand Down
52 changes: 52 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Feel free to play around with the code and fork this repl at [https://repl.it/@v
- [API](#api)
- [Building Layouts](#building-layouts)
- [Built-in Components](#built-in-components)
- [Hooks](#hooks)
- [Useful Components](#useful-components)
- [Testing](#testing)

Expand Down Expand Up @@ -838,6 +839,57 @@ Usage:
</StdoutContext.Consumer>
```
## Hooks
### useInput
This hook is used for handling user input.
It's a more convienient alternative to using `StdinContext` and listening to `data` events.
You can find a full example of using `useInput` at [examples/useinput](examples/useinput/useinput.js).
```jsx
import {useInput} from 'ink';

const UserInput = () => {
useInput((input, meta) => {
if (input === 'q') {
// Exit program
}

if (meta.left) {
// Left arrow key pressed
}
});

return
};
```
Handler function that you pass to `useInput` receives two arguments:
#### input
Type: `string`
Input that program received.
#### meta
Type: `object`
Handy input metadata. Exposes properties to detect if arrow keys were pressed.
##### meta.left
##### meta.right
##### meta.up
##### meta.down
Type: `boolean`<br>
Default: `false`
If an arrow key was pressed, corresponding property will be `true`.
For example, if user presses left arrow key, `meta.left` equals `true`.
## Useful Components
Expand Down
2 changes: 0 additions & 2 deletions src/components/App.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import readline from 'readline';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import cliCursor from 'cli-cursor';
Expand Down Expand Up @@ -97,7 +96,6 @@ export default class App extends PureComponent {
stdin.addListener('data', this.handleInput);
stdin.resume();
stdin.setRawMode(true);
readline.emitKeypressEvents(stdin);
}

this.rawModeEnabledCount++;
Expand Down
30 changes: 30 additions & 0 deletions src/hooks/useInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {useLayoutEffect, useContext} from 'react';
import {StdinContext} from '..';

export default inputHandler => {
const {stdin, setRawMode} = useContext(StdinContext);

useLayoutEffect(() => {
setRawMode(true);

return () => setRawMode(false);
}, [setRawMode]);

useLayoutEffect(() => {
const handleData = data => {
const input = String(data);
const meta = {
up: input === '\u001B[A',
down: input === '\u001B[B',
left: input === '\u001B[D',
right: input === '\u001B[C'
};

inputHandler(input, meta);
};

stdin.on('data', handleData);

return () => stdin.off('data', handleData);
}, [stdin, inputHandler]);
};
15 changes: 0 additions & 15 deletions src/hooks/useKeypress.js

This file was deleted.

2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export {default as AppContext} from './components/AppContext';
export {default as StdinContext} from './components/StdinContext';
export {default as StdoutContext} from './components/StdoutContext';
export {default as Static} from './components/Static';
export {default as useKeypress} from './hooks/useKeypress';
export {default as useInput} from './hooks/useInput';
42 changes: 0 additions & 42 deletions test/exit.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,48 +47,6 @@ test('exit with thrown error', async t => {
t.true(output.includes('errored'));
});

test.cb('handles keypresses', t => {
const term = spawn('node', ['./fixtures/run', './handles-keypresses'], {
name: 'xterm',
cols: 100,
cwd: __dirname,
env: process.env
});

let output = '';

term.on('data', data => {
output += data;
});

let isExited = false;

term.on('exit', code => {
isExited = true;

if (code === 0) {
t.true(output.includes('exited'));
t.pass();
t.end();
return;
}

t.fail();
t.end();
});

setTimeout(() => {
t.false(isExited);
'abcdefghijklmnopqrstuvwxyz'.split('').forEach(key => term.write(key));
}, 100);

setTimeout(() => {
term.kill();
t.fail();
t.end();
}, 5000);
});

test.cb('don\'t exit while raw mode is active', t => {
const term = spawn('node', ['./fixtures/run', './exit-double-raw-mode'], {
name: 'xterm-color',
Expand Down
22 changes: 0 additions & 22 deletions test/fixtures/handles-keypresses.js

This file was deleted.

22 changes: 22 additions & 0 deletions test/fixtures/handles-user-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';
const React = require('react');
const {render, useInput, AppContext} = require('../..');

const UserInput = () => {
const {exit} = React.useContext(AppContext);

useInput(input => {
if (input === 'q') {
exit();
}
});

return null;
};

const app = render(<UserInput/>);

(async () => {
await app.waitUntilExit();
console.log('exited');
})();
44 changes: 44 additions & 0 deletions test/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {serial as test} from 'ava';
import {spawn} from 'node-pty';

test.cb('exit when user types "q" character', t => {
const term = spawn('node', ['./fixtures/run', './handles-user-input'], {
name: 'xterm-color',
cols: 100,
cwd: __dirname,
env: process.env
});

let output = '';

term.on('data', data => {
output += data;
});

let isExited = false;

term.on('exit', code => {
isExited = true;

if (code === 0) {
t.true(output.includes('exited'));
t.pass();
t.end();
return;
}

t.fail();
t.end();
});

setTimeout(() => {
t.false(isExited);
term.write('q');
}, 1000);

setTimeout(() => {
term.kill();
t.fail();
t.end();
}, 1500);
});

0 comments on commit dd3efe8

Please sign in to comment.