• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayPrototypeJoin,
5  ArrayPrototypePush,
6  Promise,
7} = primordials;
8
9const { CSI } = require('internal/readline/utils');
10const { validateBoolean, validateInteger } = require('internal/validators');
11const { isWritable } = require('internal/streams/utils');
12const { codes: { ERR_INVALID_ARG_TYPE } } = require('internal/errors');
13
14const {
15  kClearToLineBeginning,
16  kClearToLineEnd,
17  kClearLine,
18  kClearScreenDown,
19} = CSI;
20
21class Readline {
22  #autoCommit = false;
23  #stream;
24  #todo = [];
25
26  constructor(stream, options = undefined) {
27    if (!isWritable(stream))
28      throw new ERR_INVALID_ARG_TYPE('stream', 'Writable', stream);
29    this.#stream = stream;
30    if (options?.autoCommit != null) {
31      validateBoolean(options.autoCommit, 'options.autoCommit');
32      this.#autoCommit = options.autoCommit;
33    }
34  }
35
36  /**
37   * Moves the cursor to the x and y coordinate on the given stream.
38   * @param {integer} x
39   * @param {integer} [y]
40   * @returns {Readline} this
41   */
42  cursorTo(x, y = undefined) {
43    validateInteger(x, 'x');
44    if (y != null) validateInteger(y, 'y');
45
46    const data = y == null ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`;
47    if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
48    else ArrayPrototypePush(this.#todo, data);
49
50    return this;
51  }
52
53  /**
54   * Moves the cursor relative to its current location.
55   * @param {integer} dx
56   * @param {integer} dy
57   * @returns {Readline} this
58   */
59  moveCursor(dx, dy) {
60    if (dx || dy) {
61      validateInteger(dx, 'dx');
62      validateInteger(dy, 'dy');
63
64      let data = '';
65
66      if (dx < 0) {
67        data += CSI`${-dx}D`;
68      } else if (dx > 0) {
69        data += CSI`${dx}C`;
70      }
71
72      if (dy < 0) {
73        data += CSI`${-dy}A`;
74      } else if (dy > 0) {
75        data += CSI`${dy}B`;
76      }
77      if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
78      else ArrayPrototypePush(this.#todo, data);
79    }
80    return this;
81  }
82
83  /**
84   * Clears the current line the cursor is on.
85   * @param {-1|0|1} dir Direction to clear:
86   *   -1 for left of the cursor
87   *   +1 for right of the cursor
88   *    0 for the entire line
89   * @returns {Readline} this
90   */
91  clearLine(dir) {
92    validateInteger(dir, 'dir', -1, 1);
93
94    const data =
95      dir < 0 ? kClearToLineBeginning :
96        dir > 0 ? kClearToLineEnd :
97          kClearLine;
98    if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
99    else ArrayPrototypePush(this.#todo, data);
100    return this;
101  }
102
103  /**
104   * Clears the screen from the current position of the cursor down.
105   * @returns {Readline} this
106   */
107  clearScreenDown() {
108    if (this.#autoCommit) {
109      process.nextTick(() => this.#stream.write(kClearScreenDown));
110    } else {
111      ArrayPrototypePush(this.#todo, kClearScreenDown);
112    }
113    return this;
114  }
115
116  /**
117   * Sends all the pending actions to the associated `stream` and clears the
118   * internal list of pending actions.
119   * @returns {Promise<void>} Resolves when all pending actions have been
120   * flushed to the associated `stream`.
121   */
122  commit() {
123    return new Promise((resolve) => {
124      this.#stream.write(ArrayPrototypeJoin(this.#todo, ''), resolve);
125      this.#todo = [];
126    });
127  }
128
129  /**
130   * Clears the internal list of pending actions without sending it to the
131   * associated `stream`.
132   * @returns {Readline} this
133   */
134  rollback() {
135    this.#todo = [];
136    return this;
137  }
138}
139
140module.exports = {
141  Readline,
142};
143