// Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. 'use strict'; const { DateNow, FunctionPrototypeBind, FunctionPrototypeCall, ObjectDefineProperties, ObjectSetPrototypeOf, Promise, PromiseReject, StringPrototypeSlice, } = primordials; const { clearLine, clearScreenDown, cursorTo, moveCursor, } = require('internal/readline/callbacks'); const emitKeypressEvents = require('internal/readline/emitKeypressEvents'); const promises = require('readline/promises'); const { AbortError, } = require('internal/errors'); const { inspect, } = require('internal/util/inspect'); const { promisify } = require('internal/util'); const { validateAbortSignal } = require('internal/validators'); /** * @typedef {import('./stream.js').Readable} Readable * @typedef {import('./stream.js').Writable} Writable */ const { Interface: _Interface, InterfaceConstructor, kAddHistory, kDecoder, kDeleteLeft, kDeleteLineLeft, kDeleteLineRight, kDeleteRight, kDeleteWordLeft, kDeleteWordRight, kGetDisplayPos, kHistoryNext, kHistoryPrev, kInsertString, kLine, kLine_buffer, kMoveCursor, kNormalWrite, kOldPrompt, kOnLine, kPreviousKey, kPrompt, kQuestionCallback, kQuestionCancel, kRefreshLine, kSawKeyPress, kSawReturnAt, kSetRawMode, kTabComplete, kTabCompleter, kTtyWrite, kWordLeft, kWordRight, kWriteToOutput, } = require('internal/readline/interface'); function Interface(input, output, completer, terminal) { if (!(this instanceof Interface)) { return new Interface(input, output, completer, terminal); } if (input?.input && typeof input.completer === 'function' && input.completer.length !== 2) { const { completer } = input; input.completer = (v, cb) => cb(null, completer(v)); } else if (typeof completer === 'function' && completer.length !== 2) { const realCompleter = completer; completer = (v, cb) => cb(null, realCompleter(v)); } FunctionPrototypeCall(InterfaceConstructor, this, input, output, completer, terminal); if (process.env.TERM === 'dumb') { this._ttyWrite = FunctionPrototypeBind(_ttyWriteDumb, this); } } ObjectSetPrototypeOf(Interface.prototype, _Interface.prototype); ObjectSetPrototypeOf(Interface, _Interface); const superQuestion = _Interface.prototype.question; /** * Displays `query` by writing it to the `output`. * @param {string} query * @param {{ signal?: AbortSignal; }} [options] * @param {Function} cb * @returns {void} */ Interface.prototype.question = function(query, options, cb) { cb = typeof options === 'function' ? options : cb; options = typeof options === 'object' && options !== null ? options : {}; if (options.signal) { validateAbortSignal(options.signal, 'options.signal'); if (options.signal.aborted) { return; } const onAbort = () => { this[kQuestionCancel](); }; options.signal.addEventListener('abort', onAbort, { once: true }); const cleanup = () => { options.signal.removeEventListener(onAbort); }; cb = typeof cb === 'function' ? (answer) => { cleanup(); return cb(answer); } : cleanup; } if (typeof cb === 'function') { FunctionPrototypeCall(superQuestion, this, query, cb); } }; Interface.prototype.question[promisify.custom] = function(query, options) { options = typeof options === 'object' && options !== null ? options : {}; if (options.signal && options.signal.aborted) { return PromiseReject( new AbortError(undefined, { cause: options.signal.reason })); } return new Promise((resolve, reject) => { let cb = resolve; if (options.signal) { const onAbort = () => { reject(new AbortError(undefined, { cause: options.signal.reason })); }; options.signal.addEventListener('abort', onAbort, { once: true }); cb = (answer) => { options.signal.removeEventListener('abort', onAbort); resolve(answer); }; } this.question(query, options, cb); }); }; /** * Creates a new `readline.Interface` instance. * @param {Readable | { * input: Readable; * output: Writable; * completer?: Function; * terminal?: boolean; * history?: string[]; * historySize?: number; * removeHistoryDuplicates?: boolean; * prompt?: string; * crlfDelay?: number; * escapeCodeTimeout?: number; * tabSize?: number; * signal?: AbortSignal; * }} input * @param {Writable} [output] * @param {Function} [completer] * @param {boolean} [terminal] * @returns {Interface} */ function createInterface(input, output, completer, terminal) { return new Interface(input, output, completer, terminal); } ObjectDefineProperties(Interface.prototype, { // Redirect internal prototype methods to the underscore notation for backward // compatibility. [kSetRawMode]: { get() { return this._setRawMode; } }, [kOnLine]: { get() { return this._onLine; } }, [kWriteToOutput]: { get() { return this._writeToOutput; } }, [kAddHistory]: { get() { return this._addHistory; } }, [kRefreshLine]: { get() { return this._refreshLine; } }, [kNormalWrite]: { get() { return this._normalWrite; } }, [kInsertString]: { get() { return this._insertString; } }, [kTabComplete]: { get() { return this._tabComplete; } }, [kWordLeft]: { get() { return this._wordLeft; } }, [kWordRight]: { get() { return this._wordRight; } }, [kDeleteLeft]: { get() { return this._deleteLeft; } }, [kDeleteRight]: { get() { return this._deleteRight; } }, [kDeleteWordLeft]: { get() { return this._deleteWordLeft; } }, [kDeleteWordRight]: { get() { return this._deleteWordRight; } }, [kDeleteLineLeft]: { get() { return this._deleteLineLeft; } }, [kDeleteLineRight]: { get() { return this._deleteLineRight; } }, [kLine]: { get() { return this._line; } }, [kHistoryNext]: { get() { return this._historyNext; } }, [kHistoryPrev]: { get() { return this._historyPrev; } }, [kGetDisplayPos]: { get() { return this._getDisplayPos; } }, [kMoveCursor]: { get() { return this._moveCursor; } }, [kTtyWrite]: { get() { return this._ttyWrite; } }, // Defining proxies for the internal instance properties for backward // compatibility. _decoder: { get() { return this[kDecoder]; }, set(value) { this[kDecoder] = value; }, }, _line_buffer: { get() { return this[kLine_buffer]; }, set(value) { this[kLine_buffer] = value; }, }, _oldPrompt: { get() { return this[kOldPrompt]; }, set(value) { this[kOldPrompt] = value; }, }, _previousKey: { get() { return this[kPreviousKey]; }, set(value) { this[kPreviousKey] = value; }, }, _prompt: { get() { return this[kPrompt]; }, set(value) { this[kPrompt] = value; }, }, _questionCallback: { get() { return this[kQuestionCallback]; }, set(value) { this[kQuestionCallback] = value; }, }, _sawKeyPress: { get() { return this[kSawKeyPress]; }, set(value) { this[kSawKeyPress] = value; }, }, _sawReturnAt: { get() { return this[kSawReturnAt]; }, set(value) { this[kSawReturnAt] = value; }, }, }); // Make internal methods public for backward compatibility. Interface.prototype._setRawMode = _Interface.prototype[kSetRawMode]; Interface.prototype._onLine = _Interface.prototype[kOnLine]; Interface.prototype._writeToOutput = _Interface.prototype[kWriteToOutput]; Interface.prototype._addHistory = _Interface.prototype[kAddHistory]; Interface.prototype._refreshLine = _Interface.prototype[kRefreshLine]; Interface.prototype._normalWrite = _Interface.prototype[kNormalWrite]; Interface.prototype._insertString = _Interface.prototype[kInsertString]; Interface.prototype._tabComplete = function(lastKeypressWasTab) { // Overriding parent method because `this.completer` in the legacy // implementation takes a callback instead of being an async function. this.pause(); const string = StringPrototypeSlice(this.line, 0, this.cursor); this.completer(string, (err, value) => { this.resume(); if (err) { this._writeToOutput(`Tab completion error: ${inspect(err)}`); return; } this[kTabCompleter](lastKeypressWasTab, value); }); }; Interface.prototype._wordLeft = _Interface.prototype[kWordLeft]; Interface.prototype._wordRight = _Interface.prototype[kWordRight]; Interface.prototype._deleteLeft = _Interface.prototype[kDeleteLeft]; Interface.prototype._deleteRight = _Interface.prototype[kDeleteRight]; Interface.prototype._deleteWordLeft = _Interface.prototype[kDeleteWordLeft]; Interface.prototype._deleteWordRight = _Interface.prototype[kDeleteWordRight]; Interface.prototype._deleteLineLeft = _Interface.prototype[kDeleteLineLeft]; Interface.prototype._deleteLineRight = _Interface.prototype[kDeleteLineRight]; Interface.prototype._line = _Interface.prototype[kLine]; Interface.prototype._historyNext = _Interface.prototype[kHistoryNext]; Interface.prototype._historyPrev = _Interface.prototype[kHistoryPrev]; Interface.prototype._getDisplayPos = _Interface.prototype[kGetDisplayPos]; Interface.prototype._getCursorPos = _Interface.prototype.getCursorPos; Interface.prototype._moveCursor = _Interface.prototype[kMoveCursor]; Interface.prototype._ttyWrite = _Interface.prototype[kTtyWrite]; function _ttyWriteDumb(s, key) { key = key || {}; if (key.name === 'escape') return; if (this[kSawReturnAt] && key.name !== 'enter') this[kSawReturnAt] = 0; if (key.ctrl) { if (key.name === 'c') { if (this.listenerCount('SIGINT') > 0) { this.emit('SIGINT'); } else { // This readline instance is finished this.close(); } return; } else if (key.name === 'd') { this.close(); return; } } switch (key.name) { case 'return': // Carriage return, i.e. \r this[kSawReturnAt] = DateNow(); this._line(); break; case 'enter': // When key interval > crlfDelay if (this[kSawReturnAt] === 0 || DateNow() - this[kSawReturnAt] > this.crlfDelay) { this._line(); } this[kSawReturnAt] = 0; break; default: if (typeof s === 'string' && s) { this.line += s; this.cursor += s.length; this._writeToOutput(s); } } } module.exports = { Interface, clearLine, clearScreenDown, createInterface, cursorTo, emitKeypressEvents, moveCursor, promises, };