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