// Derived from Node.js v24.16.0 (MIT; Copyright Node.js contributors, and Joyent, Inc. and // other Node contributors). See LICENSE-node. // node:tty. A process's stdio is either pipes, where isatty is false for every fd, or one // kernel pty, where fds 0-2 are the terminal: process.stdin is a tty.ReadStream (raw mode // is the ttySetRaw syscall) and process.stdout/stderr are tty.WriteStreams whose size the // kernel reports and refreshes on SIGWINCH. getColorDepth is Node's lib/internal/tty.js. // // Deviation: Node lets a guest open another ReadStream on fd 0; here process.stdin is the // only reader the kernel feeds, so a second one is refused rather than left silent. import type { SyscallSpec, WinSize } from "../shared/protocol"; import type { Loop } from "./loop"; import { invalidArgType } from "./errors"; import { unsupported } from "./refuse"; import { StdinStream } from "./stdin"; import { StdioWriter } from "./stdout"; import { clearLine, clearScreenDown, cursorTo, moveCursor } from "./readline/callbacks"; export interface TtyHost { loop: Loop; /** True when fds 0-2 are a pty. */ terminal: boolean; callSync(op: K, args: SyscallSpec[K]["args"]): SyscallSpec[K]["ret"]; write(fd: number, bytes: Uint8Array): void; env(): Record; emitWarning(message: string, type: string): void; } let host: TtyHost; let stdinClaimed = false; export function installTty(h: TtyHost): void { host = h; } const isTerminalFd = (fd: number): boolean => host.terminal && fd >= 0 && fd <= 2; export function isatty(fd: unknown): boolean { return Number.isInteger(fd) && (fd as number) >= 0 && (fd as number) <= 2147483647 && isTerminalFd(fd as number); } function checkFd(fd: unknown): number { if (typeof fd !== "number" || fd >> 0 !== fd || fd < 0) { throw Object.assign(new RangeError(`"fd" must be a positive integer: ${String(fd)}`), { code: "ERR_INVALID_FD" }); } if (!isTerminalFd(fd)) { // What uv_tty_init answers for a descriptor that is not a terminal. throw Object.assign(new Error("TTY initialization failed: uv_tty_init returned EINVAL (invalid argument)"), { code: "ERR_TTY_INIT_FAILED", errno: -22, syscall: "uv_tty_init", info: { errno: -22, code: "EINVAL", message: "invalid argument", syscall: "uv_tty_init" }, }); } return fd; } function readLoop(fd: unknown): Loop { checkFd(fd); if (stdinClaimed) throw unsupported("tty", "ReadStream", "process.stdin is the only reader of the terminal"); stdinClaimed = true; return host.loop; } export class ReadStream extends StdinStream { isRaw = false; readonly isTTY = true; constructor(fd: number, _options?: unknown) { // Node's tty.ReadStream reads with a high-water mark of 0. super(readLoop(fd), 0); } setRawMode(flag: unknown): this { const raw = Boolean(flag); try { host.callSync("ttySetRaw", { raw }); } catch (e) { this.emit("error", e); return this; } this.isRaw = raw; return this; } } export class WriteStream extends StdioWriter { columns: number; rows: number; constructor(fd: number) { super(checkFd(fd), (bytes) => host.write(fd, bytes), (fn) => host.loop.nextTick(fn)); const size = host.callSync("ttyGetSize", {}); this.columns = size.cols; this.rows = size.rows; } getColorDepth(env?: Record): number { return getColorDepth(env); } hasColors(count?: unknown, env?: Record): boolean { return hasColors(count, env); } _refreshSize(): void { let size: WinSize; try { size = host.callSync("ttyGetSize", {}); } catch (e) { this.emit("error", e); return; } if (this.columns !== size.cols || this.rows !== size.rows) { this.columns = size.cols; this.rows = size.rows; this.emit("resize"); } } getWindowSize(): [number, number] { return [this.columns, this.rows]; } cursorTo(x: unknown, y?: unknown, callback?: unknown): boolean { return cursorTo(this, x, y, callback); } moveCursor(dx: number, dy: number, callback?: unknown): boolean { return moveCursor(this, dx, dy, callback); } clearLine(dir: number, callback?: unknown): boolean { return clearLine(this, dir, callback); } clearScreenDown(callback?: unknown): boolean { return clearScreenDown(this, callback); } } Object.defineProperty(WriteStream.prototype, "isTTY", { value: true, writable: true, configurable: true }); // ---- colour depth (lib/internal/tty.js) ---- const COLORS_2 = 1; const COLORS_16 = 4; const COLORS_256 = 8; const COLORS_16m = 24; const TERM_ENVS: Record = { 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, "rxvt-unicode-24bit": COLORS_16m, terminator: COLORS_16m, "xterm-kitty": COLORS_16m, }; const CI_ENVS: Array<[string, number]> = [ ["APPVEYOR", COLORS_256], ["BUILDKITE", COLORS_256], ["CIRCLECI", COLORS_16m], ["DRONE", COLORS_256], ["GITEA_ACTIONS", COLORS_16m], ["GITHUB_ACTIONS", COLORS_16m], ["GITLAB_CI", COLORS_256], ["TRAVIS", COLORS_256], ]; const TERM_ENVS_REG_EXP = [/ansi/, /color/, /linux/, /direct/, /^con[0-9]*x[0-9]/, /^rxvt/, /^screen/, /^xterm/, /^vt100/, /^vt220/]; let warned = false; function warnOnDeactivatedColors(env: Record): void { if (warned) return; let name = ""; if (env.NODE_DISABLE_COLORS !== undefined && env.NODE_DISABLE_COLORS !== "") name = "NODE_DISABLE_COLORS"; if (env.NO_COLOR !== undefined && env.NO_COLOR !== "") { if (name !== "") name += "' and '"; name += "NO_COLOR"; } if (name !== "") { host.emitWarning(`The '${name}' env is ignored due to the 'FORCE_COLOR' env being set.`, "Warning"); warned = true; } } export function getColorDepth(env: Record = host.env()): number { 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 && env.NODE_DISABLE_COLORS !== "") || (env.NO_COLOR !== undefined && env.NO_COLOR !== "") || env.TERM === "dumb") { return COLORS_2; } // process.platform is linux, so the win32 branch never applies. if (env.TMUX) return COLORS_16m; if (Object.hasOwn(env, "TF_BUILD") && Object.hasOwn(env, "AGENT_NAME")) return COLORS_16; if (Object.hasOwn(env, "CI")) { for (const [name, colors] of CI_ENVS) if (Object.hasOwn(env, name)) return colors; if (env.CI_NAME === "codeship") return COLORS_256; return COLORS_2; } if ("TEAMCITY_VERSION" in env) { return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.exec(env.TEAMCITY_VERSION ?? "") !== null ? COLORS_16 : COLORS_2; } switch (env.TERM_PROGRAM) { case "iTerm.app": if (!env.TERM_PROGRAM_VERSION || /^[0-2]\./.exec(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 (/truecolor/.exec(env.TERM) !== null) return COLORS_16m; if (/^xterm-256/.exec(env.TERM) !== null) return COLORS_256; const termEnv = env.TERM.toLowerCase(); if (TERM_ENVS[termEnv]) return TERM_ENVS[termEnv]!; if (TERM_ENVS_REG_EXP.some((term) => term.exec(termEnv) !== null)) return COLORS_16; } if (env.COLORTERM) return COLORS_16; return COLORS_2; } export function hasColors(count?: unknown, env?: Record): boolean { if (env === undefined && (count === undefined || (typeof count === "object" && count !== null))) { env = count as Record | undefined; count = 16; } else if (typeof count !== "number") { throw invalidArgType("count", "of type number", count); } else if (!Number.isInteger(count) || count < 2 || count > Number.MAX_SAFE_INTEGER) { const must = Number.isInteger(count) ? ">= 2 && <= 9007199254740991" : "an integer"; throw Object.assign(new RangeError(`The value of "count" is out of range. It must be ${must}. Received ${count}`), { code: "ERR_OUT_OF_RANGE", }); } return (count as number) <= 2 ** getColorDepth(env); } /** Node's shouldColorize (lib/internal/util/colors.js): what console and styleText use. */ export function shouldColorize(stream: unknown): boolean { if (host.env().FORCE_COLOR !== undefined) return getColorDepth() > 2; const s = stream as { isTTY?: boolean; getColorDepth?: () => number } | null | undefined; return Boolean(s?.isTTY && (typeof s.getColorDepth === "function" ? s.getColorDepth() > 2 : true)); } export const tty = { isatty, ReadStream, WriteStream };