1import * as Log from '../../log';
2import { logCmdError } from '../../utils/errors';
3
4const CTRL_C = '\u0003';
5
6const debug = require('debug')('expo:start:interface:keyPressHandler') as typeof console.log;
7
8/** An abstract key stroke interceptor. */
9export class KeyPressHandler {
10  private isInterceptingKeyStrokes = false;
11  private isHandlingKeyPress = false;
12
13  constructor(public onPress: (key: string) => Promise<any>) {}
14
15  /** Start observing interaction pause listeners. */
16  createInteractionListener() {
17    // Support observing prompts.
18    let wasIntercepting = false;
19
20    const listener = ({ pause }: { pause: boolean }) => {
21      if (pause) {
22        // Track if we were already intercepting key strokes before pausing, so we can
23        // resume after pausing.
24        wasIntercepting = this.isInterceptingKeyStrokes;
25        this.stopInterceptingKeyStrokes();
26      } else if (wasIntercepting) {
27        // Only start if we were previously intercepting.
28        this.startInterceptingKeyStrokes();
29      }
30    };
31
32    return listener;
33  }
34
35  private handleKeypress = async (key: string) => {
36    // Prevent sending another event until the previous event has finished.
37    if (this.isHandlingKeyPress && key !== CTRL_C) {
38      return;
39    }
40    this.isHandlingKeyPress = true;
41    try {
42      debug(`Key pressed: ${key}`);
43      await this.onPress(key);
44    } catch (error: any) {
45      await logCmdError(error);
46    } finally {
47      this.isHandlingKeyPress = false;
48    }
49  };
50
51  /** Start intercepting all key strokes and passing them to the input `onPress` method. */
52  startInterceptingKeyStrokes() {
53    if (this.isInterceptingKeyStrokes) {
54      return;
55    }
56    this.isInterceptingKeyStrokes = true;
57    const { stdin } = process;
58    // TODO: This might be here because of an old Node version.
59    if (!stdin.setRawMode) {
60      Log.warn('Using a non-interactive terminal, keyboard commands are disabled.');
61      return;
62    }
63    stdin.setRawMode(true);
64    stdin.resume();
65    stdin.setEncoding('utf8');
66    stdin.on('data', this.handleKeypress);
67  }
68
69  /** Stop intercepting all key strokes. */
70  stopInterceptingKeyStrokes() {
71    if (!this.isInterceptingKeyStrokes) {
72      return;
73    }
74    this.isInterceptingKeyStrokes = false;
75    const { stdin } = process;
76    stdin.removeListener('data', this.handleKeypress);
77    // TODO: This might be here because of an old Node version.
78    if (!stdin.setRawMode) {
79      Log.warn('Using a non-interactive terminal, keyboard commands are disabled.');
80      return;
81    }
82    stdin.setRawMode(false);
83    stdin.resume();
84  }
85}
86