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'); +})();