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