Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add useInput hook to handle user input #227

Merged
merged 22 commits into from
Sep 26, 2019
Merged
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, key) => {
if (input === 'q') {
exit();
}

if (key.leftArrow) {
setX(Math.max(1, x - 1));
}

if (key.rightArrow) {
setX(Math.min(20, x + 1));
}

if (key.upArrow) {
setY(Math.max(1, y - 1));
}

if (key.downArrow) {
setY(Math.min(10, y + 1));
}
});

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

render(<Robot/>);
40 changes: 40 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,46 @@ export type Instance = {

export type Unmount = () => void;

/**
* This hook is used for handling user input.
* It's a more convienient alternative to using `StdinContext` and listening to `data` events.
* The callback you pass to `useInput` is called for each character when user enters any input.
* However, if user pastes text and it's more than one character, the callback will be called only once and the whole string will be passed as `input`.
*
* ```
* import {useInput} from 'ink';
*
* const UserInput = () => {
* useInput((input, key) => {
* if (input === 'q') {
* // Exit program
* }
*
* if (key.leftArrow) {
* // Left arrow key pressed
* }
* });
*
* return …
* };
* ```
*/
vadimdemedes marked this conversation as resolved.
Show resolved Hide resolved
export function useInput(
inputHandler: (input: string, key: Key) => void
): void;

export interface Key {
upArrow: boolean
downArrow: boolean
leftArrow: boolean
rightArrow: boolean
return: boolean
escape: boolean
ctrl: boolean
shift: boolean
meta: boolean
}

/**
* Mount a component and render the output.
*/
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,15 @@
"plugins": [
"react"
],
"rules": {
"react/no-unescaped-entities": "off"
},
"overrides": [
{
"files": "src/components/*.js",
"files": [
"src/components/*.js",
"src/hooks/*.js"
],
"rules": {
"unicorn/filename-case": "off",
"react/require-default-props": "warning"
Expand Down
82 changes: 82 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)
- [Experimental mode](#experimental-mode)
Expand Down Expand Up @@ -846,6 +847,87 @@ 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.
The callback you pass to `useInput` is called for each character when user enters any input.
However, if user pastes text and it's more than one character, the callback will be called only once and the whole string will be passed as `input`.
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, key) => {
if (input === 'q') {
// Exit program
}

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

return …
};
```

The handler function that you pass to `useInput` receives two arguments:

#### input

Type: `string`

The input that the program received.

#### key

Type: `object`

Handy information about a key that was pressed.

##### key.leftArrow
##### key.rightArrow
##### key.upArrow
##### key.downArrow

Type: `boolean`<br>
Default: `false`

If an arrow key was pressed, the corresponding property will be `true`.
For example, if user presses left arrow key, `key.leftArrow` equals `true`.

##### key.return

Type: `boolean`<br>
Default: `false`

Return (Enter) key was pressed.

##### key.ctrl

Type: `boolean`<br>
Default: `false`

Ctrl key was pressed.

##### key.shift

Type: `boolean`<br>
Default: `false`

Shift key was pressed.

##### key.meta

Type: `boolean`<br>
Default: `false`

[Meta key](https://en.wikipedia.org/wiki/Meta_key) was pressed.


## 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
56 changes: 56 additions & 0 deletions src/hooks/use-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {useEffect, useContext} from 'react';
import {StdinContext} from '..';

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

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

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

useEffect(() => {
const handleData = data => {
let input = String(data);
const key = {
upArrow: input === '\u001B[A',
downArrow: input === '\u001B[B',
leftArrow: input === '\u001B[D',
rightArrow: input === '\u001B[C',
return: input === '\r',
escape: input === '\u001B',
ctrl: false,
shift: false,
meta: false
};

// Copied from `keypress` module
if (input <= '\u001A' && !key.return) {
input = String.fromCharCode(input.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
key.ctrl = true;
}

if (input[0] === '\u001B') {
input = input.slice(1);
key.meta = true;
}

const isLatinUppercase = input >= 'A' && input <= 'Z';
const isCyrillicUppercase = input >= 'А' && input <= 'Я';
if (input.length === 1 && (isLatinUppercase || isCyrillicUppercase)) {
key.shift = true;
}

inputHandler(input, key);
};

stdin.on('data', handleData);

return () => {
stdin.off('data', handleData);
};
}, [stdin, inputHandler]);
};
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +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 useInput} from './hooks/use-input';
65 changes: 65 additions & 0 deletions test/fixtures/use-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';
const React = require('react');
const {render, useInput, AppContext} = require('../..');

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

useInput((input, key) => {
if (test === 'lowercase' && input === 'q') {
exit();
return;
}

if (test === 'uppercase' && input === 'Q' && key.shift) {
exit();
return;
}

if (test === 'escape' && key.escape) {
exit();
return;
}

if (test === 'ctrl' && input === 'f' && key.ctrl) {
exit();
return;
}

if (test === 'meta' && input === 'm' && key.meta) {
exit();
return;
}

if (test === 'upArrow' && key.upArrow) {
exit();
return;
}

if (test === 'downArrow' && key.downArrow) {
exit();
return;
}

if (test === 'leftArrow' && key.leftArrow) {
exit();
return;
}

if (test === 'rightArrow' && key.rightArrow) {
exit();
return;
}

throw new Error('Crash');
});

return null;
};

const app = render(<UserInput test={process.argv[3]}/>);

(async () => {
await app.waitUntilExit();
console.log('exited');
})();
Loading