tty: add raw-vt and io raw modes
Project: nodejs/node | Language: JavaScript, C++ | Concepts: api-design, backwards-compatibility, testing-strategy | Difficulty: mid
Link: https://github.com/nodejs/node/pull/64140
The problem
Node.js's tty.ReadStream#setRawMode() previously only supported a boolean to enable or disable "raw" mode, which did not distinguish between different low-level terminal modes. There was no way to explicitly request libuv's binary-safe I/O mode (UV_TTY_MODE_IO), and the API did not allow introspection of the exact current raw mode. This limited the ability to control or reason about terminal behavior, especially for advanced applications needing precise terminal handling.
Why it's non-trivial
A naive fix might just add another boolean or overload, but that would break compatibility or introduce ambiguity. The solution must:
- Maintain existing behavior for
setRawMode(true/false)for compatibility. - Allow explicit selection of different raw modes, matching libuv's capabilities.
- Expose the current mode in a way that doesn't break existing consumers.
- Properly validate arguments and handle unsupported modes.
- Update documentation and tests to cover the new cases.
The approach
1. Extend the API to accept string modes and expose the current mode:
-ReadStream.prototype.setRawMode = function(flag) {
- flag = !!flag;
- const err = this._handle?.setRawMode(flag);
+ReadStream.prototype.setRawMode = function(mode) {
+ let rawMode;
+ if (mode === 'io' || mode === 'raw') {
+ rawMode = mode;
+ } else if (typeof mode === 'string') {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'mode', mode, "must be true, false, 'raw', or 'io'");
+ } else {
+ rawMode = mode ? 'raw' : false;
+ }
+ let ttyMode = UV_TTY_MODE_NORMAL;
+ if (rawMode === 'io') {
+ ttyMode = UV_TTY_MODE_IO;
+ } else if (rawMode === 'raw') {
+ ttyMode = UV_TTY_MODE_RAW_VT;
+ }
+ const err = this._handle?.setRawMode(ttyMode);
if (err) {
this.emit('error', new ErrnoException(err, 'setRawMode'));
return this;
}
- this.isRaw = flag;
+ this.isRaw = rawMode !== false;
+ this.rawMode = rawMode;
return this;
};
This change allows setRawMode('raw') and setRawMode('io') as explicit string arguments, mapping to the appropriate libuv constants. It also introduces the rawMode property to reflect the current mode, while keeping isRaw as a boolean for compatibility.
2. Validate and reject unknown string modes:
+ if (mode === 'io' || mode === 'raw') {
+ rawMode = mode;
+ } else if (typeof mode === 'string') {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'mode', mode, "must be true, false, 'raw', or 'io'");
+ }
This ensures that only supported string modes are accepted, and unknown strings throw a clear error.
3. Expose the underlying libuv constants to JS:
+ NODE_DEFINE_CONSTANT(target, UV_TTY_MODE_NORMAL);
+ NODE_DEFINE_CONSTANT(target, UV_TTY_MODE_IO);
+ NODE_DEFINE_CONSTANT(target, UV_TTY_MODE_RAW_VT);
These lines make the C++ constants available to JS, so the mapping in JS code is robust and not hard-coded.
4. Update the C++ binding to accept the mode integer:
- int err = uv_tty_set_mode(
- &wrap->handle_,
- args[0]->IsTrue() ? UV_TTY_MODE_RAW_VT : UV_TTY_MODE_NORMAL);
+ Environment* env = Environment::GetCurrent(args);
+ int mode;
+ if (!args[0]->Int32Value(env->context()).To(&mode)) return;
+ int err = uv_tty_set_mode(&wrap->handle_, static_cast<uv_tty_mode_t>(mode));
This change allows the JS layer to pass an explicit integer mode, supporting all libuv tty modes.
5. Document the new API and add a test:
+* `mode` {boolean|string} If `true` or `'raw'`, configures the
+ `tty.ReadStream` to operate as a raw device. If `'io'`, configures the
+ `tty.ReadStream` to operate in binary-safe I/O mode. If `false`, configures
+ the `tty.ReadStream` to operate in its default mode. The `readStream.isRaw`
+ property will be set to whether the stream is in raw mode, and the
+ `readStream.rawMode` property will be set to the resulting mode.
The documentation now reflects the new string options and the rawMode property.
+process.stdin.setRawMode('raw');
+assert.strictEqual(process.stdin.isRaw, true);
+assert.strictEqual(process.stdin.rawMode, 'raw');
+
+process.stdin.setRawMode('io');
+assert.strictEqual(process.stdin.isRaw, true);
+assert.strictEqual(process.stdin.rawMode, 'io');
+
+assert.throws(
+ () => process.stdin.setRawMode('raw-vt'),
+ {
+ code: 'ERR_INVALID_ARG_VALUE',
+ name: 'TypeError',
+ });
The new test covers all supported and unsupported modes, verifying the API and error handling.
From the review thread
trivikr: This PR needs to a rebase to resolve conflicts in
doc/api/tty.md
samuel-williams-shopify: @trivikr done.
addaleax:
Not sure what's different on the Alpine runners, I guess if there's no obvious reason for this it might be fine to silence stderr forsttyinstead of specifyinginheritthere
samuel-williams-shopify:
Pushed a test-only update in 7af5c595 to capturesttystderr intest-set-raw-mode-modesinstead of inheriting it. This should prevent Alpine'sstty: standard input: Not a ttymessage from polluting the pseudo-tty expected-output comparison while still surfacing stderr ifsttyexits non-zero.
Takeaways
- When extending an API, preserve existing behavior for legacy argument types to avoid breaking current users.
- Explicitly validate and reject unsupported argument values to prevent silent bugs and improve developer experience.
- When adding new modes or options, expose the current state in a way that is both backward-compatible (e.g., keep
isRawas a boolean) and forward-looking (e.g., addrawModefor richer introspection). - Update tests and documentation together with code changes to ensure new behaviors are covered and discoverable.