diff --git a/examples/usekeypress/index.js b/examples/usekeypress/index.js
new file mode 100644
index 000000000..95364cfde
--- /dev/null
+++ b/examples/usekeypress/index.js
@@ -0,0 +1,2 @@
+'use strict';
+require('import-jsx')('./usekeypress');
diff --git a/examples/usekeypress/usekeypress.js b/examples/usekeypress/usekeypress.js
new file mode 100644
index 000000000..836131df2
--- /dev/null
+++ b/examples/usekeypress/usekeypress.js
@@ -0,0 +1,42 @@
+'use strict';
+const {useState} = require('react');
+const React = require('react');
+const {render, useKeypress, Box} = require('../..');
+
+const {exit} = process;
+
+const Robot = () => {
+ const [x, setX] = useState(1);
+ const [y, setY] = useState(1);
+
+ useKeypress(key => {
+ switch (key) {
+ case 'h':
+ setX(Math.max(1, x - 1));
+ break;
+ case 'l':
+ setX(Math.min(80, x + 1));
+ break;
+ case 'j':
+ setY(Math.min(40, y + 1));
+ break;
+ case 'k':
+ setY(Math.max(1, y - 1));
+ break;
+ case 'q':
+ exit();
+ break;
+ default:
+ break;
+ }
+ });
+
+ return (
+
+ Use h, j, k, and l to move the face. q to exit
+ ^_^
+
+ );
+};
+
+render();
diff --git a/index.d.ts b/index.d.ts
index 4817f0a52..32be616e9 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,4 +1,5 @@
import * as React from "react";
+import { Key } from 'readline';
export interface RenderOptions {
/**
@@ -44,6 +45,14 @@ export type Instance = {
export type Unmount = () => void;
+/**
+ * Hook that calls keyPressHandler callback with whatever key has been pressed.
+ * See key to handle modifiers correctly.
+ */
+export function useKeypress(
+ keyPressHandler: (str?: string, key?: Key) => void
+): void;
+
/**
* Mount a component and render the output.
*/
diff --git a/package.json b/package.json
index 9090e3820..fcfc7af2e 100644
--- a/package.json
+++ b/package.json
@@ -118,7 +118,10 @@
],
"overrides": [
{
- "files": "src/components/*.js",
+ "files": [
+ "src/components/*.js",
+ "src/hooks/*.js"
+ ],
"rules": {
"unicorn/filename-case": "off",
"react/require-default-props": "warning"
diff --git a/src/hooks/useKeypress.js b/src/hooks/useKeypress.js
new file mode 100644
index 000000000..b29fc769d
--- /dev/null
+++ b/src/hooks/useKeypress.js
@@ -0,0 +1,15 @@
+import {useEffect, useContext} from 'react';
+import {StdinContext} from '..';
+
+export default keypressHandler => {
+ const {stdin, setRawMode} = useContext(StdinContext);
+
+ useEffect(() => {
+ setRawMode(true);
+ stdin.on('keypress', keypressHandler);
+ return () => {
+ stdin.off('keypress', keypressHandler);
+ setRawMode(false);
+ };
+ }, [stdin, setRawMode, keypressHandler]);
+};
diff --git a/src/index.js b/src/index.js
index d5357a261..22486bc93 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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 useKeypress} from './hooks/useKeypress';
diff --git a/test/exit.js b/test/exit.js
index 07381e376..9fe855e7c 100644
--- a/test/exit.js
+++ b/test/exit.js
@@ -47,6 +47,48 @@ 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',
diff --git a/test/fixtures/handles-keypresses.js b/test/fixtures/handles-keypresses.js
new file mode 100644
index 000000000..567822141
--- /dev/null
+++ b/test/fixtures/handles-keypresses.js
@@ -0,0 +1,22 @@
+'use strict';
+const React = require('react');
+const {render, useKeypress} = require('../..');
+
+const input = new Set('abcdefghijklmnopqrstuvwxyz'.split(''));
+
+const KeypressTest = () => {
+ useKeypress(str => {
+ input.delete(str);
+ if (input.size === 0) {
+ app.unmount();
+ }
+ });
+ return null;
+};
+
+const app = render();
+
+(async () => {
+ await app.waitUntilExit();
+ console.log('exited');
+})();