Skip to main content

term/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Functionality to assist with managing the terminal/console/tty.
5
6// UNSAFETY: Win32 and libc function calls to manipulate terminal state.
7#![expect(unsafe_code)]
8
9/// Enables VT and UTF-8 output.
10#[cfg(windows)]
11pub fn enable_vt_and_utf8() {
12    use windows_sys::Win32::Globalization::CP_UTF8;
13    use windows_sys::Win32::System::Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING;
14    use windows_sys::Win32::System::Console::GetConsoleMode;
15    use windows_sys::Win32::System::Console::GetStdHandle;
16    use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE;
17    use windows_sys::Win32::System::Console::SetConsoleMode;
18    use windows_sys::Win32::System::Console::SetConsoleOutputCP;
19    // SAFETY: calling Windows APIs as documented.
20    unsafe {
21        let conout = GetStdHandle(STD_OUTPUT_HANDLE);
22        let mut mode = 0;
23        if GetConsoleMode(conout, &mut mode) != 0 {
24            if mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0 {
25                SetConsoleMode(conout, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
26            }
27            SetConsoleOutputCP(CP_UTF8);
28        }
29    }
30}
31
32/// Enables VT and UTF-8 output. No-op on non-Windows platforms.
33#[cfg(not(windows))]
34pub fn enable_vt_and_utf8() {}
35
36/// Clones `file` into a `File`.
37///
38/// # Safety
39/// The caller must ensure `file` owns a valid file.
40#[cfg(windows)]
41fn clone_file(file: impl std::os::windows::io::AsHandle) -> std::fs::File {
42    file.as_handle().try_clone_to_owned().unwrap().into()
43}
44
45/// Clones `file` into a `File`.
46///
47/// # Safety
48/// The caller must ensure `file` owns a valid file.
49#[cfg(unix)]
50fn clone_file(file: impl std::os::unix::io::AsFd) -> std::fs::File {
51    file.as_fd().try_clone_to_owned().unwrap().into()
52}
53
54/// Returns a non-buffering stdout, with no special console handling on Windows.
55pub fn raw_stdout() -> std::fs::File {
56    clone_file(std::io::stdout())
57}
58
59/// Returns a non-buffering stderr, with no special console handling on Windows.
60pub fn raw_stderr() -> std::fs::File {
61    clone_file(std::io::stderr())
62}
63
64/// Sets a panic handler to restore the terminal state when the process panics.
65#[cfg(unix)]
66pub fn revert_terminal_on_panic() {
67    let orig_termios = get_termios();
68
69    let base_hook = std::panic::take_hook();
70    std::panic::set_hook(Box::new(move |info| {
71        eprintln!("restoring terminal attributes on panic...");
72        set_termios(orig_termios);
73        base_hook(info)
74    }));
75}
76
77/// Opaque wrapper around `libc::termios`.
78#[cfg(unix)]
79#[derive(Copy, Clone)]
80pub struct Termios(libc::termios);
81
82/// Get the current termios settings for stderr.
83#[cfg(unix)]
84pub fn get_termios() -> Termios {
85    let mut orig_termios = std::mem::MaybeUninit::<libc::termios>::uninit();
86    // SAFETY: `tcgetattr` has no preconditions, and stderr has been checked to be a tty
87    let ret = unsafe { libc::tcgetattr(libc::STDERR_FILENO, orig_termios.as_mut_ptr()) };
88    if ret != 0 {
89        panic!(
90            "error: could not save term attributes: {}",
91            std::io::Error::last_os_error()
92        );
93    }
94    // SAFETY: `tcgetattr` returned successfully, therefore `orig_termios` has been initialized
95    let orig_termios = unsafe { orig_termios.assume_init() };
96    Termios(orig_termios)
97}
98
99/// Set the termios settings for stderr.
100#[cfg(unix)]
101pub fn set_termios(termios: Termios) {
102    // SAFETY: stderr is guaranteed to be an open fd, and `termios` is a valid termios struct.
103    let ret = unsafe { libc::tcsetattr(libc::STDERR_FILENO, libc::TCSAFLUSH, &termios.0) };
104    if ret != 0 {
105        panic!(
106            "error: could not restore term attributes via tcsetattr: {}",
107            std::io::Error::last_os_error()
108        );
109    }
110}
111
112/// Opens a PTY pair, returning `(primary, secondary)`.
113///
114/// Both fds have `O_CLOEXEC` set atomically at open time so they
115/// cannot leak into child processes even under concurrent `fork`.
116/// Callers that need the secondary in a child should pass it via
117/// `Stdio::from()`, which `dup2`s it onto stdin/stdout/stderr.
118#[cfg(unix)]
119pub fn open_pty() -> std::io::Result<(std::fs::File, std::fs::File)> {
120    use std::ffi::CStr;
121    use std::ffi::OsStr;
122    use std::os::unix::ffi::OsStrExt as _;
123    use std::os::unix::fs::OpenOptionsExt;
124    use std::os::unix::io::AsRawFd;
125    use std::os::unix::io::FromRawFd;
126
127    // Use the POSIX flow (posix_openpt + grantpt + unlockpt + open)
128    // instead of openpty() so we can pass O_CLOEXEC at open time,
129    // avoiding a race between open and fcntl.
130
131    // SAFETY: posix_openpt is called with valid flags.
132    let primary_fd = unsafe { libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY | libc::O_CLOEXEC) };
133    if primary_fd < 0 {
134        return Err(std::io::Error::last_os_error());
135    }
136    // SAFETY: primary_fd is valid from the successful posix_openpt call.
137    let primary = unsafe { std::fs::File::from_raw_fd(primary_fd) };
138
139    // SAFETY: the fd is valid. grantpt/unlockpt have no preconditions
140    // beyond a valid primary fd.
141    unsafe {
142        if libc::grantpt(primary.as_raw_fd()) != 0 {
143            return Err(std::io::Error::last_os_error());
144        }
145        if libc::unlockpt(primary.as_raw_fd()) != 0 {
146            return Err(std::io::Error::last_os_error());
147        }
148    }
149
150    // ptsname_r is missing from libc for macos, despite being present, and
151    // it's very hard to get improvements upstream. So, just define it here.
152    #[cfg(not(target_os = "macos"))]
153    use libc::ptsname_r;
154    #[cfg(target_os = "macos")]
155    unsafe extern "C" {
156        unsafe fn ptsname_r(fd: i32, buf: *mut std::ffi::c_char, buflen: usize) -> i32;
157    }
158
159    // Get the secondary device name using ptsname_r (thread-safe).
160    let mut name_buf = [0u8; 128];
161    // SAFETY: ptsname_r writes into the provided buffer and null-terminates.
162    let ret = unsafe {
163        ptsname_r(
164            primary.as_raw_fd(),
165            name_buf.as_mut_ptr().cast(),
166            name_buf.len(),
167        )
168    };
169    if ret != 0 {
170        return Err(std::io::Error::from_raw_os_error(ret));
171    }
172
173    let name = CStr::from_bytes_until_nul(&name_buf).expect("libc contract violation");
174    let secondary = std::fs::OpenOptions::new()
175        .read(true)
176        .write(true)
177        .custom_flags(libc::O_NOCTTY)
178        .open(OsStr::from_bytes(name.to_bytes()))?;
179
180    Ok((primary, secondary))
181}