Skip to content

Commit 7f0b2c0

Browse files
eweilowVadim Demedes
authored and
Vadim Demedes
committed
Fix setRawMode with non-TTY stdin and expose isRawModeSupported on StdinContext (#172)
1 parent 5929aab commit 7f0b2c0

File tree

4 files changed

+178
-3
lines changed

4 files changed

+178
-3
lines changed

index.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,15 @@ export const StdinContext: React.Context<{
199199
* Stdin stream passed to `render()` in `options.stdin` or `process.stdin` by default. Useful if your app needs to handle user input.
200200
*/
201201
readonly stdin: NodeJS.ReadStream;
202+
203+
/**
204+
* A boolean flag determining if the current `stdin` supports `setRawMode`. A component using `setRawMode` might want to use `isRawModeSupported` to nicely fall back in environments where raw mode is not supported.
205+
*/
206+
readonly isRawModeSupported: boolean;
207+
202208
/**
203209
* Ink exposes this function via own `<StdinContext>` to be able to handle Ctrl+C, that's why you should use Ink's `setRawMode` instead of `process.stdin.setRawMode`.
210+
* If the `stdin` stream passed to Ink does not support setRawMode, this function does nothing.
204211
*/
205212
readonly setRawMode: NodeJS.ReadStream["setRawMode"];
206213
}>;

readme.md

+17
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,23 @@ Usage:
767767
</StdinContext.Consumer>
768768
```
769769

770+
##### isRawModeSupported
771+
772+
Type: `boolean`
773+
774+
A boolean flag determining if the current `stdin` supports `setRawMode`.
775+
A component using `setRawMode` might want to use `isRawModeSupported` to nicely fall back in environments where raw mode is not supported.
776+
777+
Usage:
778+
779+
```jsx
780+
<StdinContext.Consumer>
781+
{({ isRawModeSupported, setRawMode, stdin }) => (
782+
isRawModeSupported ? <MyInputComponent setRawMode={setRawMode} stdin={stdin}/> : <MyComponentThatDoesntUseInput />
783+
)}
784+
</StdinContext.Consumer>
785+
```
786+
770787
##### setRawMode
771788

772789
Type: `function`<br>

src/components/App.js

+28-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ export default class App extends PureComponent {
1818
onExit: PropTypes.func.isRequired
1919
};
2020

21+
// Determines if TTY is supported on the provided stdin
22+
isRawModeSupported() {
23+
return this.props.stdin.isTTY;
24+
}
25+
2126
constructor() {
2227
super();
2328

@@ -36,7 +41,8 @@ export default class App extends PureComponent {
3641
<StdinContext.Provider
3742
value={{
3843
stdin: this.props.stdin,
39-
setRawMode: this.handleSetRawMode
44+
setRawMode: this.handleSetRawMode,
45+
isRawModeSupported: this.isRawModeSupported()
4046
}}
4147
>
4248
<StdoutContext.Provider
@@ -57,7 +63,11 @@ export default class App extends PureComponent {
5763

5864
componentWillUnmount() {
5965
cliCursor.show(this.props.stdout);
60-
this.handleSetRawMode(false);
66+
67+
// ignore calling setRawMode on an handle stdin it cannot be called
68+
if (this.isRawModeSupported()) {
69+
this.handleSetRawMode(false);
70+
}
6171
}
6272

6373
componentDidCatch(error) {
@@ -67,6 +77,18 @@ export default class App extends PureComponent {
6777
handleSetRawMode = isEnabled => {
6878
const {stdin} = this.props;
6979

80+
if (!this.isRawModeSupported()) {
81+
if (stdin === process.stdin) {
82+
throw new Error(
83+
'Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported'
84+
);
85+
} else {
86+
throw new Error(
87+
'Raw mode is not supported on the stdin provided to Ink.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported'
88+
);
89+
}
90+
}
91+
7092
stdin.setEncoding('utf8');
7193

7294
if (isEnabled) {
@@ -98,7 +120,10 @@ export default class App extends PureComponent {
98120
};
99121

100122
handleExit = error => {
101-
this.handleSetRawMode(false);
123+
if (this.isRawModeSupported()) {
124+
this.handleSetRawMode(false);
125+
}
126+
102127
this.props.onExit(error);
103128
}
104129
}

test/components.js

+126
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ test('disable raw mode when all input components are unmounted', t => {
261261
const stdin = new EventEmitter();
262262
stdin.setEncoding = () => {};
263263
stdin.setRawMode = spy();
264+
stdin.isTTY = true; // Without this, setRawMode will throw
264265
stdin.resume = spy();
265266
stdin.pause = spy();
266267

@@ -316,6 +317,131 @@ test('disable raw mode when all input components are unmounted', t => {
316317
t.true(stdin.pause.calledOnce);
317318
});
318319

320+
test('setRawMode() should throw if raw mode is not supported', t => {
321+
const stdout = {
322+
write: spy(),
323+
columns: 100
324+
};
325+
326+
const stdin = new EventEmitter();
327+
stdin.setEncoding = () => {};
328+
stdin.setRawMode = spy();
329+
stdin.isTTY = false;
330+
stdin.resume = spy();
331+
stdin.pause = spy();
332+
333+
const didCatchInMount = spy();
334+
const didCatchInUnmount = spy();
335+
336+
const options = {
337+
stdout,
338+
stdin,
339+
debug: true
340+
};
341+
342+
class Input extends React.Component {
343+
render() {
344+
return <Box>Test</Box>;
345+
}
346+
347+
componentDidMount() {
348+
try {
349+
this.props.setRawMode(true);
350+
} catch (error) {
351+
didCatchInMount(error);
352+
}
353+
}
354+
355+
componentWillUnmount() {
356+
try {
357+
this.props.setRawMode(false);
358+
} catch (error) {
359+
didCatchInUnmount(error);
360+
}
361+
}
362+
}
363+
364+
const Test = () => (
365+
<StdinContext.Consumer>
366+
{({setRawMode}) => (
367+
<Input setRawMode={setRawMode}/>
368+
)}
369+
</StdinContext.Consumer>
370+
);
371+
372+
const {unmount} = render(<Test/>, options);
373+
unmount();
374+
375+
t.is(didCatchInMount.callCount, 1);
376+
t.is(didCatchInUnmount.callCount, 1);
377+
t.false(stdin.setRawMode.called);
378+
t.false(stdin.resume.called);
379+
t.false(stdin.pause.called);
380+
});
381+
382+
test('render different component based on whether stdin is a TTY or not', t => {
383+
const stdout = {
384+
write: spy(),
385+
columns: 100
386+
};
387+
388+
const stdin = new EventEmitter();
389+
stdin.setEncoding = () => {};
390+
stdin.setRawMode = spy();
391+
stdin.isTTY = false;
392+
stdin.resume = spy();
393+
stdin.pause = spy();
394+
395+
const options = {
396+
stdout,
397+
stdin,
398+
debug: true
399+
};
400+
401+
class Input extends React.Component {
402+
render() {
403+
return <Box>Test</Box>;
404+
}
405+
406+
componentDidMount() {
407+
this.props.setRawMode(true);
408+
}
409+
410+
componentWillUnmount() {
411+
this.props.setRawMode(false);
412+
}
413+
}
414+
415+
const Test = ({renderFirstInput, renderSecondInput}) => (
416+
<StdinContext.Consumer>
417+
{({isRawModeSupported, setRawMode}) => (
418+
<>
419+
{isRawModeSupported && renderFirstInput && <Input setRawMode={setRawMode}/>}
420+
{isRawModeSupported && renderSecondInput && <Input setRawMode={setRawMode}/>}
421+
</>
422+
)}
423+
</StdinContext.Consumer>
424+
);
425+
426+
const {rerender} = render(<Test renderFirstInput renderSecondInput/>, options);
427+
428+
t.false(stdin.setRawMode.called);
429+
t.false(stdin.resume.called);
430+
t.false(stdin.pause.called);
431+
432+
rerender(<Test renderFirstInput/>);
433+
434+
t.false(stdin.setRawMode.called);
435+
t.false(stdin.resume.called);
436+
t.false(stdin.pause.called);
437+
438+
rerender(<Test/>);
439+
440+
t.false(stdin.setRawMode.called);
441+
t.false(stdin.resume.called);
442+
t.false(stdin.pause.called);
443+
});
444+
319445
test('render only last frame when run in CI', async t => {
320446
const output = await run('ci', {
321447
env: {CI: true}

0 commit comments

Comments
 (0)