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

Fix setRawMode with non-TTY stdin and expose isRawModeSupported on StdinContext #172

Merged
merged 19 commits into from
May 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,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 @@ -762,6 +762,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