aboutsummaryrefslogtreecommitdiff
path: root/src/js/node/tty.js
diff options
context:
space:
mode:
authorGravatar Dylan Conway <35280289+dylan-conway@users.noreply.github.com> 2023-08-19 00:11:24 -0700
committerGravatar GitHub <noreply@github.com> 2023-08-19 00:11:24 -0700
commitdb09ed15fd561b89b24b979b986e21a04576f7cc (patch)
treeafe32e4eccaf1fe3f2f4cd536c1f2a61efad28c1 /src/js/node/tty.js
parentbf517d9f8e993a9ed3a02d21094c3ce76d7953a1 (diff)
downloadbun-db09ed15fd561b89b24b979b986e21a04576f7cc.tar.gz
bun-db09ed15fd561b89b24b979b986e21a04576f7cc.tar.zst
bun-db09ed15fd561b89b24b979b986e21a04576f7cc.zip
tty `ReadStream`, `WriteStream`, and readline rawmode (#4179)
* tty `WriteStream`, `ReadStream`, and rawmode * tests * refactor prototypes * fix failing test * fix test and library usage * more merge * fix child_process test * create pseudo terminal for tty tests * match node logic * handle invalid tty * close descriptors * move tests to another process * fix test again * fix test on linux
Diffstat (limited to 'src/js/node/tty.js')
-rw-r--r--src/js/node/tty.js289
1 files changed, 289 insertions, 0 deletions
diff --git a/src/js/node/tty.js b/src/js/node/tty.js
new file mode 100644
index 000000000..09010e8fc
--- /dev/null
+++ b/src/js/node/tty.js
@@ -0,0 +1,289 @@
+const { ttySetMode, isatty, getWindowSize: _getWindowSize } = $lazy("tty");
+
+function ReadStream(fd) {
+ if (!(this instanceof ReadStream)) return new ReadStream(fd);
+ if (fd >> 0 !== fd || fd < 0) throw new RangeError("fd must be a positive integer");
+
+ const stream = require("node:fs").ReadStream.call(this, `/dev/fd/${fd}`);
+
+ stream.isRaw = false;
+ stream.isTTY = isatty(stream.fd);
+
+ return stream;
+}
+
+Object.defineProperty(ReadStream, "prototype", {
+ get() {
+ const Real = require("node:fs").ReadStream.prototype;
+
+ Object.defineProperty(ReadStream, "prototype", { value: Real });
+ ReadStream.prototype.setRawMode = function (flag) {
+ const mode = flag ? 1 : 0;
+ const err = ttySetMode(this.fd, mode);
+ if (err) {
+ this.emit("error", new Error("setRawMode failed with errno:", err));
+ return this;
+ }
+ this.isRaw = flag;
+ return this;
+ };
+
+ return Real;
+ },
+ enumerable: true,
+ configurable: true,
+});
+
+let OSRelease;
+
+const COLORS_2 = 1;
+const COLORS_16 = 4;
+const COLORS_256 = 8;
+const COLORS_16m = 24;
+
+// Some entries were taken from `dircolors`
+// (https://linux.die.net/man/1/dircolors). The corresponding terminals might
+// support more than 16 colors, but this was not tested for.
+//
+// Copyright (C) 1996-2016 Free Software Foundation, Inc. Copying and
+// distribution of this file, with or without modification, are permitted
+// provided the copyright notice and this notice are preserved.
+const TERM_ENVS = {
+ "eterm": COLORS_16,
+ "cons25": COLORS_16,
+ "console": COLORS_16,
+ "cygwin": COLORS_16,
+ "dtterm": COLORS_16,
+ "gnome": COLORS_16,
+ "hurd": COLORS_16,
+ "jfbterm": COLORS_16,
+ "konsole": COLORS_16,
+ "kterm": COLORS_16,
+ "mlterm": COLORS_16,
+ "mosh": COLORS_16m,
+ "putty": COLORS_16,
+ "st": COLORS_16,
+ // https://github.com/da-x/rxvt-unicode/tree/v9.22-with-24bit-color
+ "rxvt-unicode-24bit": COLORS_16m,
+ // https://gist.github.com/XVilka/8346728#gistcomment-2823421
+ "terminator": COLORS_16m,
+};
+
+const TERM_ENVS_REG_EXP = [/ansi/, /color/, /linux/, /^con[0-9]*x[0-9]/, /^rxvt/, /^screen/, /^xterm/, /^vt100/];
+
+let warned = false;
+function warnOnDeactivatedColors(env) {
+ if (warned) return;
+ let name = "";
+ if (env.NODE_DISABLE_COLORS !== undefined) name = "NODE_DISABLE_COLORS";
+ if (env.NO_COLOR !== undefined) {
+ if (name !== "") {
+ name += "' and '";
+ }
+ name += "NO_COLOR";
+ }
+
+ if (name !== "") {
+ process.emitWarning(`The '${name}' env is ignored due to the 'FORCE_COLOR' env being set.`, "Warning");
+ warned = true;
+ }
+}
+
+function WriteStream(fd) {
+ if (!(this instanceof WriteStream)) return new WriteStream(fd);
+ if (fd >> 0 !== fd || fd < 0) throw new RangeError("fd must be a positive integer");
+
+ const stream = require("node:fs").WriteStream.call(this, `/dev/fd/${fd}`);
+
+ stream.columns = undefined;
+ stream.rows = undefined;
+ stream.isTTY = isatty(stream.fd);
+
+ if (stream.isTTY) {
+ const windowSizeArray = [0, 0];
+ if (_getWindowSize(fd, windowSizeArray) === true) {
+ stream.columns = windowSizeArray[0];
+ stream.rows = windowSizeArray[1];
+ }
+ }
+
+ return stream;
+}
+
+Object.defineProperty(WriteStream, "prototype", {
+ get() {
+ const Real = require("node:fs").WriteStream.prototype;
+ Object.defineProperty(WriteStream, "prototype", { value: Real });
+
+ WriteStream.prototype._refreshSize = function () {
+ const oldCols = this.columns;
+ const oldRows = this.rows;
+ const windowSizeArray = [0, 0];
+ if (_getWindowSize(this.fd, windowSizeArray) === true) {
+ if (oldCols !== windowSizeArray[0] || oldRows !== windowSizeArray[1]) {
+ this.columns = windowSizeArray[0];
+ this.rows = windowSizeArray[1];
+ this.emit("resize");
+ }
+ }
+ };
+
+ var readline = undefined;
+ WriteStream.prototype.clearLine = function (dir, cb) {
+ return (readline ??= require("node:readline")).clearLine(this, dir, cb);
+ };
+
+ WriteStream.prototype.clearScreenDown = function (cb) {
+ return (readline ??= require("node:readline")).clearScreenDown(this, cb);
+ };
+
+ WriteStream.prototype.cursorTo = function (x, y, cb) {
+ return (readline ??= require("node:readline")).cursorTo(this, x, y, cb);
+ };
+
+ // The `getColorDepth` API got inspired by multiple sources such as
+ // https://github.com/chalk/supports-color,
+ // https://github.com/isaacs/color-support.
+ WriteStream.prototype.getColorDepth = function (env = process.env) {
+ // Use level 0-3 to support the same levels as `chalk` does. This is done for
+ // consistency throughout the ecosystem.
+ if (env.FORCE_COLOR !== undefined) {
+ switch (env.FORCE_COLOR) {
+ case "":
+ case "1":
+ case "true":
+ warnOnDeactivatedColors(env);
+ return COLORS_16;
+ case "2":
+ warnOnDeactivatedColors(env);
+ return COLORS_256;
+ case "3":
+ warnOnDeactivatedColors(env);
+ return COLORS_16m;
+ default:
+ return COLORS_2;
+ }
+ }
+
+ if (
+ env.NODE_DISABLE_COLORS !== undefined ||
+ // See https://no-color.org/
+ env.NO_COLOR !== undefined ||
+ // The "dumb" special terminal, as defined by terminfo, doesn't support
+ // ANSI color control codes.
+ // See https://invisible-island.net/ncurses/terminfo.ti.html#toc-_Specials
+ env.TERM === "dumb"
+ ) {
+ return COLORS_2;
+ }
+
+ if (process.platform === "win32") {
+ // Lazy load for startup performance.
+ if (OSRelease === undefined) {
+ const { release } = require("node:os");
+ OSRelease = StringPrototypeSplit(release(), ".");
+ }
+ // Windows 10 build 10586 is the first Windows release that supports 256
+ // colors. Windows 10 build 14931 is the first release that supports
+ // 16m/TrueColor.
+ if (+OSRelease[0] >= 10) {
+ const build = +OSRelease[2];
+ if (build >= 14931) return COLORS_16m;
+ if (build >= 10586) return COLORS_256;
+ }
+
+ return COLORS_16;
+ }
+
+ if (env.TMUX) {
+ return COLORS_256;
+ }
+
+ if (env.CI) {
+ if (
+ ["APPVEYOR", "BUILDKITE", "CIRCLECI", "DRONE", "GITHUB_ACTIONS", "GITLAB_CI", "TRAVIS"].some(
+ sign => sign in env,
+ ) ||
+ env.CI_NAME === "codeship"
+ ) {
+ return COLORS_256;
+ }
+ return COLORS_2;
+ }
+
+ if ("TEAMCITY_VERSION" in env) {
+ return RegExpPrototypeExec(/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/, env.TEAMCITY_VERSION) !== null
+ ? COLORS_16
+ : COLORS_2;
+ }
+
+ switch (env.TERM_PROGRAM) {
+ case "iTerm.app":
+ if (!env.TERM_PROGRAM_VERSION || RegExpPrototypeExec(/^[0-2]\./, env.TERM_PROGRAM_VERSION) !== null) {
+ return COLORS_256;
+ }
+ return COLORS_16m;
+ case "HyperTerm":
+ case "MacTerm":
+ return COLORS_16m;
+ case "Apple_Terminal":
+ return COLORS_256;
+ }
+
+ if (env.COLORTERM === "truecolor" || env.COLORTERM === "24bit") {
+ return COLORS_16m;
+ }
+
+ if (env.TERM) {
+ if (RegExpPrototypeExec(/^xterm-256/, env.TERM) !== null) {
+ return COLORS_256;
+ }
+
+ const termEnv = StringPrototypeToLowerCase(env.TERM);
+
+ if (TERM_ENVS[termEnv]) {
+ return TERM_ENVS[termEnv];
+ }
+ if (ArrayPrototypeSome(TERM_ENVS_REG_EXP, term => RegExpPrototypeExec(term, termEnv) !== null)) {
+ return COLORS_16;
+ }
+ }
+ // Move 16 color COLORTERM below 16m and 256
+ if (env.COLORTERM) {
+ return COLORS_16;
+ }
+ return COLORS_2;
+ };
+
+ WriteStream.prototype.getWindowSize = function () {
+ return [this.columns, this.rows];
+ };
+
+ WriteStream.prototype.hasColors = function (count, env) {
+ if (env === undefined && (count === undefined || (typeof count === "object" && count !== null))) {
+ env = count;
+ count = 16;
+ } else {
+ validateInteger(count, "count", 2);
+ }
+
+ return count <= 2 ** this.getColorDepth(env);
+ };
+
+ WriteStream.prototype.moveCursor = function (dx, dy, cb) {
+ return (readline ??= require("node:readline")).moveCursor(this, dx, dy, cb);
+ };
+
+ return Real;
+ },
+ enumerable: true,
+ configurable: true,
+});
+
+var validateInteger = (value, name, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) => {
+ if (typeof value !== "number") throw new ERR_INVALID_ARG_TYPE(name, "number", value);
+ if (!Number.isInteger(value)) throw new ERR_OUT_OF_RANGE(name, "an integer", value);
+ if (value < min || value > max) throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value);
+};
+
+export default { ReadStream, WriteStream, isatty };