Skip to content

Commit

Permalink
Fix setRawMode with non-TTY stdin and expose isRawModeSupported on St…
Browse files Browse the repository at this point in the history
…dinContext (vadimdemedes#172)
  • Loading branch information
eweilow authored and Jonathan Dahan committed Sep 7, 2019
1 parent 55a0342 commit 26dedb3
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 3 deletions.
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,15 @@ export const StdinContext: React.Context<{
* Stdin stream passed to `render()` in `options.stdin` or `process.stdin` by default. Useful if your app needs to handle user input.
*/
readonly stdin: NodeJS.ReadStream;

/**
* 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.
*/
readonly isRawModeSupported: boolean;

/**
* 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`.
* If the `stdin` stream passed to Ink does not support setRawMode, this function does nothing.
*/
readonly setRawMode: NodeJS.ReadStream["setRawMode"];
}>;
Expand Down
17 changes: 17 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,23 @@ Usage:
</StdinContext.Consumer>
```

##### isRawModeSupported

Type: `boolean`

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.

Usage:

```jsx
<StdinContext.Consumer>
{({ isRawModeSupported, setRawMode, stdin }) => (
isRawModeSupported ? <MyInputComponent setRawMode={setRawMode} stdin={stdin}/> : <MyComponentThatDoesntUseInput />
)}
</StdinContext.Consumer>
```

##### setRawMode

Type: `function`<br>
Expand Down
31 changes: 28 additions & 3 deletions src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export default class App extends PureComponent {
onExit: PropTypes.func.isRequired
};

// Determines if TTY is supported on the provided stdin
isRawModeSupported() {
return this.props.stdin.isTTY;
}

constructor() {
super();

Expand All @@ -36,7 +41,8 @@ export default class App extends PureComponent {
<StdinContext.Provider
value={{
stdin: this.props.stdin,
setRawMode: this.handleSetRawMode
setRawMode: this.handleSetRawMode,
isRawModeSupported: this.isRawModeSupported()
}}
>
<StdoutContext.Provider
Expand All @@ -57,7 +63,11 @@ export default class App extends PureComponent {

componentWillUnmount() {
cliCursor.show(this.props.stdout);
this.handleSetRawMode(false);

// ignore calling setRawMode on an handle stdin it cannot be called
if (this.isRawModeSupported()) {
this.handleSetRawMode(false);
}
}

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

if (!this.isRawModeSupported()) {
if (stdin === process.stdin) {
throw new Error(
'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'
);
} else {
throw new Error(
'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'
);
}
}

stdin.setEncoding('utf8');

if (isEnabled) {
Expand Down Expand Up @@ -98,7 +120,10 @@ export default class App extends PureComponent {
};

handleExit = error => {
this.handleSetRawMode(false);
if (this.isRawModeSupported()) {
this.handleSetRawMode(false);
}

this.props.onExit(error);
}
}
126 changes: 126 additions & 0 deletions test/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ test('disable raw mode when all input components are unmounted', t => {
const stdin = new EventEmitter();
stdin.setEncoding = () => {};
stdin.setRawMode = spy();
stdin.isTTY = true; // Without this, setRawMode will throw
stdin.resume = spy();
stdin.pause = spy();

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

test('setRawMode() should throw if raw mode is not supported', t => {
const stdout = {
write: spy(),
columns: 100
};

const stdin = new EventEmitter();
stdin.setEncoding = () => {};
stdin.setRawMode = spy();
stdin.isTTY = false;
stdin.resume = spy();
stdin.pause = spy();

const didCatchInMount = spy();
const didCatchInUnmount = spy();

const options = {
stdout,
stdin,
debug: true
};

class Input extends React.Component {
render() {
return <Box>Test</Box>;
}

componentDidMount() {
try {
this.props.setRawMode(true);
} catch (error) {
didCatchInMount(error);
}
}

componentWillUnmount() {
try {
this.props.setRawMode(false);
} catch (error) {
didCatchInUnmount(error);
}
}
}

const Test = () => (
<StdinContext.Consumer>
{({setRawMode}) => (
<Input setRawMode={setRawMode}/>
)}
</StdinContext.Consumer>
);

const {unmount} = render(<Test/>, options);
unmount();

t.is(didCatchInMount.callCount, 1);
t.is(didCatchInUnmount.callCount, 1);
t.false(stdin.setRawMode.called);
t.false(stdin.resume.called);
t.false(stdin.pause.called);
});

test('render different component based on whether stdin is a TTY or not', t => {
const stdout = {
write: spy(),
columns: 100
};

const stdin = new EventEmitter();
stdin.setEncoding = () => {};
stdin.setRawMode = spy();
stdin.isTTY = false;
stdin.resume = spy();
stdin.pause = spy();

const options = {
stdout,
stdin,
debug: true
};

class Input extends React.Component {
render() {
return <Box>Test</Box>;
}

componentDidMount() {
this.props.setRawMode(true);
}

componentWillUnmount() {
this.props.setRawMode(false);
}
}

const Test = ({renderFirstInput, renderSecondInput}) => (
<StdinContext.Consumer>
{({isRawModeSupported, setRawMode}) => (
<>
{isRawModeSupported && renderFirstInput && <Input setRawMode={setRawMode}/>}
{isRawModeSupported && renderSecondInput && <Input setRawMode={setRawMode}/>}
</>
)}
</StdinContext.Consumer>
);

const {rerender} = render(<Test renderFirstInput renderSecondInput/>, options);

t.false(stdin.setRawMode.called);
t.false(stdin.resume.called);
t.false(stdin.pause.called);

rerender(<Test renderFirstInput/>);

t.false(stdin.setRawMode.called);
t.false(stdin.resume.called);
t.false(stdin.pause.called);

rerender(<Test/>);

t.false(stdin.setRawMode.called);
t.false(stdin.resume.called);
t.false(stdin.pause.called);
});

test('render only last frame when run in CI', async t => {
const output = await run('ci', {
env: {CI: true}
Expand Down

0 comments on commit 26dedb3

Please sign in to comment.