18d307f52SEvan Baconimport * as Log from '../../log';
28d307f52SEvan Baconimport { logCmdError } from '../../utils/errors';
38d307f52SEvan Bacon
411a5a4d2SEvan Baconconst CTRL_C = '\u0003';
511a5a4d2SEvan Bacon
6*474a7a4bSEvan Baconconst debug = require('debug')('expo:start:interface:keyPressHandler') as typeof console.log;
7*474a7a4bSEvan Bacon
88d307f52SEvan Bacon/** An abstract key stroke interceptor. */
98d307f52SEvan Baconexport class KeyPressHandler {
108d307f52SEvan Bacon  private isInterceptingKeyStrokes = false;
118d307f52SEvan Bacon  private isHandlingKeyPress = false;
128d307f52SEvan Bacon
138d307f52SEvan Bacon  constructor(public onPress: (key: string) => Promise<any>) {}
148d307f52SEvan Bacon
158d307f52SEvan Bacon  /** Start observing interaction pause listeners. */
168d307f52SEvan Bacon  createInteractionListener() {
178d307f52SEvan Bacon    // Support observing prompts.
188d307f52SEvan Bacon    let wasIntercepting = false;
198d307f52SEvan Bacon
208d307f52SEvan Bacon    const listener = ({ pause }: { pause: boolean }) => {
218d307f52SEvan Bacon      if (pause) {
228d307f52SEvan Bacon        // Track if we were already intercepting key strokes before pausing, so we can
238d307f52SEvan Bacon        // resume after pausing.
248d307f52SEvan Bacon        wasIntercepting = this.isInterceptingKeyStrokes;
258d307f52SEvan Bacon        this.stopInterceptingKeyStrokes();
268d307f52SEvan Bacon      } else if (wasIntercepting) {
278d307f52SEvan Bacon        // Only start if we were previously intercepting.
288d307f52SEvan Bacon        this.startInterceptingKeyStrokes();
298d307f52SEvan Bacon      }
308d307f52SEvan Bacon    };
318d307f52SEvan Bacon
328d307f52SEvan Bacon    return listener;
338d307f52SEvan Bacon  }
348d307f52SEvan Bacon
358d307f52SEvan Bacon  private handleKeypress = async (key: string) => {
368d307f52SEvan Bacon    // Prevent sending another event until the previous event has finished.
3711a5a4d2SEvan Bacon    if (this.isHandlingKeyPress && key !== CTRL_C) {
388d307f52SEvan Bacon      return;
398d307f52SEvan Bacon    }
408d307f52SEvan Bacon    this.isHandlingKeyPress = true;
418d307f52SEvan Bacon    try {
42*474a7a4bSEvan Bacon      debug(`Key pressed: ${key}`);
438d307f52SEvan Bacon      await this.onPress(key);
4429975bfdSEvan Bacon    } catch (error: any) {
4529975bfdSEvan Bacon      await logCmdError(error);
468d307f52SEvan Bacon    } finally {
478d307f52SEvan Bacon      this.isHandlingKeyPress = false;
488d307f52SEvan Bacon    }
498d307f52SEvan Bacon  };
508d307f52SEvan Bacon
518d307f52SEvan Bacon  /** Start intercepting all key strokes and passing them to the input `onPress` method. */
528d307f52SEvan Bacon  startInterceptingKeyStrokes() {
538d307f52SEvan Bacon    if (this.isInterceptingKeyStrokes) {
548d307f52SEvan Bacon      return;
558d307f52SEvan Bacon    }
568d307f52SEvan Bacon    this.isInterceptingKeyStrokes = true;
578d307f52SEvan Bacon    const { stdin } = process;
588d307f52SEvan Bacon    // TODO: This might be here because of an old Node version.
598d307f52SEvan Bacon    if (!stdin.setRawMode) {
608d307f52SEvan Bacon      Log.warn('Using a non-interactive terminal, keyboard commands are disabled.');
618d307f52SEvan Bacon      return;
628d307f52SEvan Bacon    }
638d307f52SEvan Bacon    stdin.setRawMode(true);
648d307f52SEvan Bacon    stdin.resume();
658d307f52SEvan Bacon    stdin.setEncoding('utf8');
668d307f52SEvan Bacon    stdin.on('data', this.handleKeypress);
678d307f52SEvan Bacon  }
688d307f52SEvan Bacon
698d307f52SEvan Bacon  /** Stop intercepting all key strokes. */
708d307f52SEvan Bacon  stopInterceptingKeyStrokes() {
718d307f52SEvan Bacon    if (!this.isInterceptingKeyStrokes) {
728d307f52SEvan Bacon      return;
738d307f52SEvan Bacon    }
748d307f52SEvan Bacon    this.isInterceptingKeyStrokes = false;
758d307f52SEvan Bacon    const { stdin } = process;
768d307f52SEvan Bacon    stdin.removeListener('data', this.handleKeypress);
778d307f52SEvan Bacon    // TODO: This might be here because of an old Node version.
788d307f52SEvan Bacon    if (!stdin.setRawMode) {
798d307f52SEvan Bacon      Log.warn('Using a non-interactive terminal, keyboard commands are disabled.');
808d307f52SEvan Bacon      return;
818d307f52SEvan Bacon    }
828d307f52SEvan Bacon    stdin.setRawMode(false);
838d307f52SEvan Bacon    stdin.resume();
848d307f52SEvan Bacon  }
858d307f52SEvan Bacon}
86