-
Notifications
You must be signed in to change notification settings - Fork 906
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: enable pausing handling keystrokes in watch mode (#2041)
* feat: enable pausing handling keystrokes in watch mode * fix: add proper error handling * chore: code review improvements.
- Loading branch information
1 parent
92b85e6
commit 4b281bd
Showing
4 changed files
with
165 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { | ||
CLIError, | ||
addInteractionListener, | ||
logger, | ||
} from '@react-native-community/cli-tools'; | ||
|
||
/** An abstract key stroke interceptor. */ | ||
export class KeyPressHandler { | ||
private isInterceptingKeyStrokes = false; | ||
|
||
constructor(public onPress: (key: string) => void) {} | ||
|
||
/** Start observing interaction pause listeners. */ | ||
createInteractionListener() { | ||
// Support observing prompts. | ||
let wasIntercepting = false; | ||
|
||
const listener = ({pause}: {pause: boolean}) => { | ||
if (pause) { | ||
// Track if we were already intercepting key strokes before pausing, so we can | ||
// resume after pausing. | ||
wasIntercepting = this.isInterceptingKeyStrokes; | ||
this.stopInterceptingKeyStrokes(); | ||
} else if (wasIntercepting) { | ||
// Only start if we were previously intercepting. | ||
this.startInterceptingKeyStrokes(); | ||
} | ||
}; | ||
|
||
addInteractionListener(listener); | ||
} | ||
|
||
private handleKeypress = async (key: string) => { | ||
try { | ||
logger.debug(`Key pressed: ${key}`); | ||
this.onPress(key); | ||
} catch (error) { | ||
return new CLIError( | ||
'There was an error with the key press handler.', | ||
(error as Error).message, | ||
); | ||
} finally { | ||
return; | ||
} | ||
}; | ||
|
||
/** Start intercepting all key strokes and passing them to the input `onPress` method. */ | ||
startInterceptingKeyStrokes() { | ||
if (this.isInterceptingKeyStrokes) { | ||
return; | ||
} | ||
this.isInterceptingKeyStrokes = true; | ||
const {stdin} = process; | ||
stdin.setRawMode(true); | ||
stdin.resume(); | ||
stdin.setEncoding('utf8'); | ||
stdin.on('data', this.handleKeypress); | ||
} | ||
|
||
/** Stop intercepting all key strokes. */ | ||
stopInterceptingKeyStrokes() { | ||
if (!this.isInterceptingKeyStrokes) { | ||
return; | ||
} | ||
this.isInterceptingKeyStrokes = false; | ||
const {stdin} = process; | ||
stdin.removeListener('data', this.handleKeypress); | ||
stdin.setRawMode(false); | ||
stdin.resume(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import prompts, {Options, PromptObject} from 'prompts'; | ||
import {CLIError} from './errors'; | ||
import logger from './logger'; | ||
|
||
type PromptOptions = {nonInteractiveHelp?: string} & Options; | ||
type InteractionOptions = {pause: boolean; canEscape?: boolean}; | ||
type InteractionCallback = (options: InteractionOptions) => void; | ||
|
||
/** Interaction observers for detecting when keystroke tracking should pause/resume. */ | ||
const listeners: InteractionCallback[] = []; | ||
|
||
export async function prompt( | ||
question: PromptObject, | ||
options: PromptOptions = {}, | ||
) { | ||
pauseInteractions(); | ||
try { | ||
const results = await prompts(question, { | ||
onCancel() { | ||
throw new CLIError('Prompt cancelled.'); | ||
}, | ||
...options, | ||
}); | ||
|
||
return results; | ||
} finally { | ||
resumeInteractions(); | ||
} | ||
} | ||
|
||
export function pauseInteractions( | ||
options: Omit<InteractionOptions, 'pause'> = {}, | ||
) { | ||
logger.debug('Interaction observers paused'); | ||
for (const listener of listeners) { | ||
listener({pause: true, ...options}); | ||
} | ||
} | ||
|
||
/** Notify all listeners that keypress observations can start.. */ | ||
export function resumeInteractions( | ||
options: Omit<InteractionOptions, 'pause'> = {}, | ||
) { | ||
logger.debug('Interaction observers resumed'); | ||
for (const listener of listeners) { | ||
listener({pause: false, ...options}); | ||
} | ||
} | ||
|
||
/** Used to pause/resume interaction observers while prompting (made for TerminalUI). */ | ||
export function addInteractionListener(callback: InteractionCallback) { | ||
listeners.push(callback); | ||
} |