summaryrefslogtreecommitdiff
path: root/ext/node/polyfills/_readline.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'ext/node/polyfills/_readline.mjs')
-rw-r--r--ext/node/polyfills/_readline.mjs486
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,
+};