diff options
Diffstat (limited to 'ext/node/polyfills/_readline.mjs')
-rw-r--r-- | ext/node/polyfills/_readline.mjs | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/ext/node/polyfills/_readline.mjs b/ext/node/polyfills/_readline.mjs new file mode 100644 index 000000000..6e0968af0 --- /dev/null +++ b/ext/node/polyfills/_readline.mjs @@ -0,0 +1,486 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// 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. + +// deno-lint-ignore-file camelcase + +import { + clearLine, + clearScreenDown, + cursorTo, + moveCursor, +} from "internal:deno_node/polyfills/internal/readline/callbacks.mjs"; +import { emitKeypressEvents } from "internal:deno_node/polyfills/internal/readline/emitKeypressEvents.mjs"; +import promises from "internal:deno_node/polyfills/readline/promises.ts"; +import { validateAbortSignal } from "internal:deno_node/polyfills/internal/validators.mjs"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; +import { AbortError } from "internal:deno_node/polyfills/internal/errors.ts"; +import { process } from "internal:deno_node/polyfills/process.ts"; + +import { + Interface as _Interface, + InterfaceConstructor, + kAddHistory, + kDecoder, + kDeleteLeft, + kDeleteLineLeft, + kDeleteLineRight, + kDeleteRight, + kDeleteWordLeft, + kDeleteWordRight, + kGetDisplayPos, + kHistoryNext, + kHistoryPrev, + kInsertString, + kLine, + kLine_buffer, + kMoveCursor, + kNormalWrite, + kOldPrompt, + kOnLine, + kPreviousKey, + kPrompt, + kQuestion, + kQuestionCallback, + kQuestionCancel, + kRefreshLine, + kSawKeyPress, + kSawReturnAt, + kSetRawMode, + kTabComplete, + kTabCompleter, + kTtyWrite, + kWordLeft, + kWordRight, + kWriteToOutput, +} from "internal:deno_node/polyfills/internal/readline/interface.mjs"; + +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)); + } + + // NOTE(bartlomieju): in Node this is `FunctionPrototypeCall(...)`, + // but trying to do `Function.prototype.call()` somehow doesn't work here + // /shrug + InterfaceConstructor.bind( + this, + )( + input, + output, + completer, + terminal, + ); + if (process.env.TERM === "dumb") { + this._ttyWrite = _ttyWriteDumb.bind(this); + } +} + +Object.setPrototypeOf(Interface.prototype, _Interface.prototype); +Object.setPrototypeOf(Interface, _Interface); + +/** + * Displays `query` by writing it to the `output`. + * @param {string} query + * @param {{ signal?: AbortSignal; }} [options] + * @param {Function} cb + * @returns {void} + */ +Interface.prototype.question = function question(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") { + this[kQuestion](query, cb); + } +}; +Interface.prototype.question[promisify.custom] = function question( + query, + options, +) { + options = typeof options === "object" && options !== null ? options : {}; + + if (options.signal && options.signal.aborted) { + return Promise.reject( + 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); +} + +Object.defineProperties(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 = this.line.slice(0, this.cursor); + this.completer(string, (err, value) => { + this.resume(); + + if (err) { + // TODO(bartlomieju): inspect is not ported yet + // this._writeToOutput(`Tab completion error: ${inspect(err)}`); + this._writeToOutput(`Tab completion error: ${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] = Date.now(); + this._line(); + break; + + case "enter": + // When key interval > crlfDelay + if ( + this[kSawReturnAt] === 0 || + Date.now() - 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); + } + } +} + +export { + clearLine, + clearScreenDown, + createInterface, + cursorTo, + emitKeypressEvents, + Interface, + moveCursor, + promises, +}; |