Skip to main content

nodejs/stdlib/
tty.rs

1//! Node `tty` module.
2//!
3//! `tty.isatty(fd)` queries whether a file descriptor is a terminal (via
4//! `libc::isatty`). `tty.ReadStream`/`tty.WriteStream` are exposed as native
5//! classes: a `WriteStream` carries `fd`/`isTTY`/`columns`/`rows` and the
6//! cursor/erase methods (`cursorTo`/`moveCursor`/`clearLine`/`clearScreenDown`/
7//! `getWindowSize`/`getColorDepth`/`hasColors`), all routed — like
8//! `process.stdout` — through `process::stream_instance_call`; a `ReadStream`
9//! carries `fd`/`isTTY`/`isRaw` with a best-effort `setRawMode`.
10
11use crate::host::{with_host, JsObj};
12use fusevm::Value;
13use indexmap::IndexMap;
14
15pub const METHODS: &[&str] = &["isatty"];
16
17/// Instance methods of a `tty.ReadStream`.
18pub const READ_STREAM_METHODS: &[&str] = &[
19    "setRawMode",
20    "on",
21    "once",
22    "removeListener",
23    "pause",
24    "resume",
25    "setEncoding",
26    "ref",
27    "unref",
28    "destroy",
29    "read",
30];
31
32pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
33    Some(match method {
34        "isatty" => {
35            let fd = super::arg_num(args, 0);
36            let is = if fd.is_finite() {
37                // SAFETY: isatty is a pure query on the given fd number.
38                unsafe { libc::isatty(fd as libc::c_int) == 1 }
39            } else {
40                false
41            };
42            Ok(Value::Bool(is))
43        }
44        _ => return None,
45    })
46}
47
48/// `require('tty').ReadStream` / `.WriteStream` — expose the classes as
49/// constructible builtins (the parent routes `new`/instance calls by tag).
50pub fn constant(name: &str) -> Option<Value> {
51    match name {
52        "ReadStream" | "WriteStream" => Some(with_host(|h| h.alloc(JsObj::Builtin(name.into())))),
53        _ => None,
54    }
55}
56
57/// `new tty.ReadStream(fd)` / `new tty.WriteStream(fd)`.
58pub fn construct(name: &str, args: &[Value]) -> Value {
59    let fd = {
60        let n = super::arg_num(args, 0);
61        if n.is_finite() {
62            n as i32
63        } else if name == "WriteStream" {
64            1
65        } else {
66            0
67        }
68    };
69    match name {
70        "ReadStream" => read_stream(fd),
71        _ => write_stream(fd),
72    }
73}
74
75/// Build a `tty.WriteStream` object (same shape as `process.stdout`).
76pub fn write_stream(fd: i32) -> Value {
77    // SAFETY: isatty is a pure query on the fd number.
78    let is_tty = unsafe { libc::isatty(fd) == 1 };
79    let size = if is_tty { window_size(fd) } else { None };
80    with_host(|h| {
81        let mut m = IndexMap::new();
82        m.insert("@@native".into(), h.new_str("WriteStream"));
83        m.insert("fd".into(), Value::Float(fd as f64));
84        m.insert("isTTY".into(), Value::Bool(is_tty));
85        m.insert("writable".into(), Value::Bool(true));
86        if let Some((cols, rows)) = size {
87            m.insert("columns".into(), Value::Float(cols as f64));
88            m.insert("rows".into(), Value::Float(rows as f64));
89        }
90        h.new_object(m)
91    })
92}
93
94/// Build a `tty.ReadStream` object.
95pub fn read_stream(fd: i32) -> Value {
96    // SAFETY: isatty is a pure query on the fd number.
97    let is_tty = unsafe { libc::isatty(fd) == 1 };
98    with_host(|h| {
99        let mut m = IndexMap::new();
100        m.insert("@@native".into(), h.new_str("ReadStream"));
101        m.insert("fd".into(), Value::Float(fd as f64));
102        m.insert("isTTY".into(), Value::Bool(is_tty));
103        m.insert("isRaw".into(), Value::Bool(false));
104        m.insert("readable".into(), Value::Bool(true));
105        h.new_object(m)
106    })
107}
108
109/// Dispatch a `tty.ReadStream` instance method. Terminal raw-mode toggling has no
110/// termios substrate here, so `setRawMode` just records the flag; the rest are
111/// chainable no-ops so `.on('data')`/`.pause()`/`.resume()` chains load.
112pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
113    match method {
114        "setRawMode" => {
115            let mode = super::arg_num(args, 0) != 0.0;
116            with_host(|h| {
117                if let Some(JsObj::Object(p)) = h.get_mut(recv) {
118                    p.insert("isRaw".into(), Value::Bool(mode));
119                }
120            });
121            Ok(recv.clone())
122        }
123        "read" => Ok(Value::Undef),
124        "on" | "once" | "removeListener" | "pause" | "resume" | "setEncoding" | "ref" | "unref"
125        | "destroy" => Ok(recv.clone()),
126        _ => Err(crate::host::type_error(&format!(
127            "{method} is not a function"
128        ))),
129    }
130}
131
132/// The terminal's `(columns, rows)` via `ioctl(TIOCGWINSZ)`; `None` when `fd` is
133/// not a terminal or the ioctl fails.
134pub fn window_size(fd: i32) -> Option<(u16, u16)> {
135    // SAFETY: `ws` is zeroed then filled by the kernel; a failed ioctl returns -1.
136    unsafe {
137        let mut ws: libc::winsize = std::mem::zeroed();
138        if libc::ioctl(fd, libc::TIOCGWINSZ as _, &mut ws as *mut libc::winsize) == 0
139            && ws.ws_col > 0
140        {
141            Some((ws.ws_col, ws.ws_row))
142        } else {
143            None
144        }
145    }
146}