• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright Joyent, Inc. and other Node contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a
4// copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to permit
8// persons to whom the Software is furnished to do so, subject to the
9// following conditions:
10//
11// The above copyright notice and this permission notice shall be included
12// in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20// USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22'use strict';
23
24const {
25  DateNow,
26  FunctionPrototypeBind,
27  FunctionPrototypeCall,
28  ObjectDefineProperties,
29  ObjectSetPrototypeOf,
30  Promise,
31  PromiseReject,
32  StringPrototypeSlice,
33  SymbolDispose,
34} = primordials;
35
36const {
37  clearLine,
38  clearScreenDown,
39  cursorTo,
40  moveCursor,
41} = require('internal/readline/callbacks');
42const emitKeypressEvents = require('internal/readline/emitKeypressEvents');
43const promises = require('readline/promises');
44
45const {
46  AbortError,
47} = require('internal/errors');
48const {
49  inspect,
50} = require('internal/util/inspect');
51const {
52  kEmptyObject,
53  promisify,
54} = require('internal/util');
55const { validateAbortSignal } = require('internal/validators');
56
57/**
58 * @typedef {import('./stream.js').Readable} Readable
59 * @typedef {import('./stream.js').Writable} Writable
60 */
61
62const {
63  Interface: _Interface,
64  InterfaceConstructor,
65  kAddHistory,
66  kDecoder,
67  kDeleteLeft,
68  kDeleteLineLeft,
69  kDeleteLineRight,
70  kDeleteRight,
71  kDeleteWordLeft,
72  kDeleteWordRight,
73  kGetDisplayPos,
74  kHistoryNext,
75  kHistoryPrev,
76  kInsertString,
77  kLine,
78  kLine_buffer,
79  kMoveCursor,
80  kNormalWrite,
81  kOldPrompt,
82  kOnLine,
83  kPreviousKey,
84  kPrompt,
85  kQuestionCallback,
86  kQuestionCancel,
87  kRefreshLine,
88  kSawKeyPress,
89  kSawReturnAt,
90  kSetRawMode,
91  kTabComplete,
92  kTabCompleter,
93  kTtyWrite,
94  kWordLeft,
95  kWordRight,
96  kWriteToOutput,
97} = require('internal/readline/interface');
98let addAbortListener;
99
100function Interface(input, output, completer, terminal) {
101  if (!(this instanceof Interface)) {
102    return new Interface(input, output, completer, terminal);
103  }
104
105  if (input?.input &&
106      typeof input.completer === 'function' && input.completer.length !== 2) {
107    const { completer } = input;
108    input.completer = (v, cb) => cb(null, completer(v));
109  } else if (typeof completer === 'function' && completer.length !== 2) {
110    const realCompleter = completer;
111    completer = (v, cb) => cb(null, realCompleter(v));
112  }
113
114  FunctionPrototypeCall(InterfaceConstructor, this,
115                        input, output, completer, terminal);
116
117  if (process.env.TERM === 'dumb') {
118    this._ttyWrite = FunctionPrototypeBind(_ttyWriteDumb, this);
119  }
120}
121
122ObjectSetPrototypeOf(Interface.prototype, _Interface.prototype);
123ObjectSetPrototypeOf(Interface, _Interface);
124
125const superQuestion = _Interface.prototype.question;
126
127/**
128 * Displays `query` by writing it to the `output`.
129 * @param {string} query
130 * @param {{ signal?: AbortSignal; }} [options]
131 * @param {Function} cb
132 * @returns {void}
133 */
134Interface.prototype.question = function(query, options, cb) {
135  cb = typeof options === 'function' ? options : cb;
136  if (options === null || typeof options !== 'object') {
137    options = kEmptyObject;
138  }
139
140  if (options.signal) {
141    validateAbortSignal(options.signal, 'options.signal');
142    if (options.signal.aborted) {
143      return;
144    }
145
146    const onAbort = () => {
147      this[kQuestionCancel]();
148    };
149    addAbortListener ??= require('events').addAbortListener;
150    const disposable = addAbortListener(options.signal, onAbort);
151    const originalCb = cb;
152    cb = typeof cb === 'function' ? (answer) => {
153      disposable[SymbolDispose]();
154      return originalCb(answer);
155    } : disposable[SymbolDispose];
156  }
157
158  if (typeof cb === 'function') {
159    FunctionPrototypeCall(superQuestion, this, query, cb);
160  }
161};
162Interface.prototype.question[promisify.custom] = function question(query, options) {
163  if (options === null || typeof options !== 'object') {
164    options = kEmptyObject;
165  }
166
167  if (options.signal && options.signal.aborted) {
168    return PromiseReject(
169      new AbortError(undefined, { cause: options.signal.reason }));
170  }
171
172  return new Promise((resolve, reject) => {
173    let cb = resolve;
174
175    if (options.signal) {
176      const onAbort = () => {
177        reject(new AbortError(undefined, { cause: options.signal.reason }));
178      };
179      addAbortListener ??= require('events').addAbortListener;
180      const disposable = addAbortListener(options.signal, onAbort);
181      cb = (answer) => {
182        disposable[SymbolDispose]();
183        resolve(answer);
184      };
185    }
186
187    this.question(query, options, cb);
188  });
189};
190
191/**
192 * Creates a new `readline.Interface` instance.
193 * @param {Readable | {
194 *   input: Readable;
195 *   output: Writable;
196 *   completer?: Function;
197 *   terminal?: boolean;
198 *   history?: string[];
199 *   historySize?: number;
200 *   removeHistoryDuplicates?: boolean;
201 *   prompt?: string;
202 *   crlfDelay?: number;
203 *   escapeCodeTimeout?: number;
204 *   tabSize?: number;
205 *   signal?: AbortSignal;
206 *   }} input
207 * @param {Writable} [output]
208 * @param {Function} [completer]
209 * @param {boolean} [terminal]
210 * @returns {Interface}
211 */
212function createInterface(input, output, completer, terminal) {
213  return new Interface(input, output, completer, terminal);
214}
215
216ObjectDefineProperties(Interface.prototype, {
217  // Redirect internal prototype methods to the underscore notation for backward
218  // compatibility.
219  [kSetRawMode]: {
220    __proto__: null,
221    get() {
222      return this._setRawMode;
223    },
224  },
225  [kOnLine]: {
226    __proto__: null,
227    get() {
228      return this._onLine;
229    },
230  },
231  [kWriteToOutput]: {
232    __proto__: null,
233    get() {
234      return this._writeToOutput;
235    },
236  },
237  [kAddHistory]: {
238    __proto__: null,
239    get() {
240      return this._addHistory;
241    },
242  },
243  [kRefreshLine]: {
244    __proto__: null,
245    get() {
246      return this._refreshLine;
247    },
248  },
249  [kNormalWrite]: {
250    __proto__: null,
251    get() {
252      return this._normalWrite;
253    },
254  },
255  [kInsertString]: {
256    __proto__: null,
257    get() {
258      return this._insertString;
259    },
260  },
261  [kTabComplete]: {
262    __proto__: null,
263    get() {
264      return this._tabComplete;
265    },
266  },
267  [kWordLeft]: {
268    __proto__: null,
269    get() {
270      return this._wordLeft;
271    },
272  },
273  [kWordRight]: {
274    __proto__: null,
275    get() {
276      return this._wordRight;
277    },
278  },
279  [kDeleteLeft]: {
280    __proto__: null,
281    get() {
282      return this._deleteLeft;
283    },
284  },
285  [kDeleteRight]: {
286    __proto__: null,
287    get() {
288      return this._deleteRight;
289    },
290  },
291  [kDeleteWordLeft]: {
292    __proto__: null,
293    get() {
294      return this._deleteWordLeft;
295    },
296  },
297  [kDeleteWordRight]: {
298    __proto__: null,
299    get() {
300      return this._deleteWordRight;
301    },
302  },
303  [kDeleteLineLeft]: {
304    __proto__: null,
305    get() {
306      return this._deleteLineLeft;
307    },
308  },
309  [kDeleteLineRight]: {
310    __proto__: null,
311    get() {
312      return this._deleteLineRight;
313    },
314  },
315  [kLine]: {
316    __proto__: null,
317    get() {
318      return this._line;
319    },
320  },
321  [kHistoryNext]: {
322    __proto__: null,
323    get() {
324      return this._historyNext;
325    },
326  },
327  [kHistoryPrev]: {
328    __proto__: null,
329    get() {
330      return this._historyPrev;
331    },
332  },
333  [kGetDisplayPos]: {
334    __proto__: null,
335    get() {
336      return this._getDisplayPos;
337    },
338  },
339  [kMoveCursor]: {
340    __proto__: null,
341    get() {
342      return this._moveCursor;
343    },
344  },
345  [kTtyWrite]: {
346    __proto__: null,
347    get() {
348      return this._ttyWrite;
349    },
350  },
351
352  // Defining proxies for the internal instance properties for backward
353  // compatibility.
354  _decoder: {
355    __proto__: null,
356    get() {
357      return this[kDecoder];
358    },
359    set(value) {
360      this[kDecoder] = value;
361    },
362  },
363  _line_buffer: {
364    __proto__: null,
365    get() {
366      return this[kLine_buffer];
367    },
368    set(value) {
369      this[kLine_buffer] = value;
370    },
371  },
372  _oldPrompt: {
373    __proto__: null,
374    get() {
375      return this[kOldPrompt];
376    },
377    set(value) {
378      this[kOldPrompt] = value;
379    },
380  },
381  _previousKey: {
382    __proto__: null,
383    get() {
384      return this[kPreviousKey];
385    },
386    set(value) {
387      this[kPreviousKey] = value;
388    },
389  },
390  _prompt: {
391    __proto__: null,
392    get() {
393      return this[kPrompt];
394    },
395    set(value) {
396      this[kPrompt] = value;
397    },
398  },
399  _questionCallback: {
400    __proto__: null,
401    get() {
402      return this[kQuestionCallback];
403    },
404    set(value) {
405      this[kQuestionCallback] = value;
406    },
407  },
408  _sawKeyPress: {
409    __proto__: null,
410    get() {
411      return this[kSawKeyPress];
412    },
413    set(value) {
414      this[kSawKeyPress] = value;
415    },
416  },
417  _sawReturnAt: {
418    __proto__: null,
419    get() {
420      return this[kSawReturnAt];
421    },
422    set(value) {
423      this[kSawReturnAt] = value;
424    },
425  },
426});
427
428// Make internal methods public for backward compatibility.
429Interface.prototype._setRawMode = _Interface.prototype[kSetRawMode];
430Interface.prototype._onLine = _Interface.prototype[kOnLine];
431Interface.prototype._writeToOutput = _Interface.prototype[kWriteToOutput];
432Interface.prototype._addHistory = _Interface.prototype[kAddHistory];
433Interface.prototype._refreshLine = _Interface.prototype[kRefreshLine];
434Interface.prototype._normalWrite = _Interface.prototype[kNormalWrite];
435Interface.prototype._insertString = _Interface.prototype[kInsertString];
436Interface.prototype._tabComplete = function(lastKeypressWasTab) {
437  // Overriding parent method because `this.completer` in the legacy
438  // implementation takes a callback instead of being an async function.
439  this.pause();
440  const string = StringPrototypeSlice(this.line, 0, this.cursor);
441  this.completer(string, (err, value) => {
442    this.resume();
443
444    if (err) {
445      this._writeToOutput(`Tab completion error: ${inspect(err)}`);
446      return;
447    }
448
449    this[kTabCompleter](lastKeypressWasTab, value);
450  });
451};
452Interface.prototype._wordLeft = _Interface.prototype[kWordLeft];
453Interface.prototype._wordRight = _Interface.prototype[kWordRight];
454Interface.prototype._deleteLeft = _Interface.prototype[kDeleteLeft];
455Interface.prototype._deleteRight = _Interface.prototype[kDeleteRight];
456Interface.prototype._deleteWordLeft = _Interface.prototype[kDeleteWordLeft];
457Interface.prototype._deleteWordRight = _Interface.prototype[kDeleteWordRight];
458Interface.prototype._deleteLineLeft = _Interface.prototype[kDeleteLineLeft];
459Interface.prototype._deleteLineRight = _Interface.prototype[kDeleteLineRight];
460Interface.prototype._line = _Interface.prototype[kLine];
461Interface.prototype._historyNext = _Interface.prototype[kHistoryNext];
462Interface.prototype._historyPrev = _Interface.prototype[kHistoryPrev];
463Interface.prototype._getDisplayPos = _Interface.prototype[kGetDisplayPos];
464Interface.prototype._getCursorPos = _Interface.prototype.getCursorPos;
465Interface.prototype._moveCursor = _Interface.prototype[kMoveCursor];
466Interface.prototype._ttyWrite = _Interface.prototype[kTtyWrite];
467
468function _ttyWriteDumb(s, key) {
469  key = key || kEmptyObject;
470
471  if (key.name === 'escape') return;
472
473  if (this[kSawReturnAt] && key.name !== 'enter')
474    this[kSawReturnAt] = 0;
475
476  if (key.ctrl) {
477    if (key.name === 'c') {
478      if (this.listenerCount('SIGINT') > 0) {
479        this.emit('SIGINT');
480      } else {
481        // This readline instance is finished
482        this.close();
483      }
484
485      return;
486    } else if (key.name === 'd') {
487      this.close();
488      return;
489    }
490  }
491
492  switch (key.name) {
493    case 'return':  // Carriage return, i.e. \r
494      this[kSawReturnAt] = DateNow();
495      this._line();
496      break;
497
498    case 'enter':
499      // When key interval > crlfDelay
500      if (this[kSawReturnAt] === 0 ||
501          DateNow() - this[kSawReturnAt] > this.crlfDelay) {
502        this._line();
503      }
504      this[kSawReturnAt] = 0;
505      break;
506
507    default:
508      if (typeof s === 'string' && s) {
509        this.line += s;
510        this.cursor += s.length;
511        this._writeToOutput(s);
512      }
513  }
514}
515
516module.exports = {
517  Interface,
518  clearLine,
519  clearScreenDown,
520  createInterface,
521  cursorTo,
522  emitKeypressEvents,
523  moveCursor,
524  promises,
525};
526