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
9use thiserror::Error;
10
11// Errors for terminal operations.
12#[derive(Error, Debug)]
13#[expect(missing_docs)]
14pub enum Error {
15    #[error("failed to perform a virtual terminal operation: {0}")]
16    VtOperationFailed(std::io::Error),
17}
18
19/// Enables VT and UTF-8 output.
20#[cfg(windows)]
21pub fn enable_vt_and_utf8() {
22    use winapi::um::consoleapi;
23    use winapi::um::processenv;
24    use winapi::um::winbase;
25    use winapi::um::wincon;
26    use winapi::um::winnls;
27    // SAFETY: calling Windows APIs as documented.
28    unsafe {
29        let conout = processenv::GetStdHandle(winbase::STD_OUTPUT_HANDLE);
30        let mut mode = 0;
31        if consoleapi::GetConsoleMode(conout, &mut mode) != 0 {
32            if mode & wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0 {
33                consoleapi::SetConsoleMode(
34                    conout,
35                    mode | wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING,
36                );
37            }
38            wincon::SetConsoleOutputCP(winnls::CP_UTF8);
39        }
40    }
41}
42
43/// Enables VT and UTF-8 output. No-op on non-Windows platforms.
44#[cfg(not(windows))]
45pub fn enable_vt_and_utf8() {}
46
47/// Enables or disables raw console mode.
48pub fn set_raw_console(enable: bool) -> Result<(), Error> {
49    if enable {
50        crossterm::terminal::enable_raw_mode().map_err(Error::VtOperationFailed)
51    } else {
52        crossterm::terminal::disable_raw_mode().map_err(Error::VtOperationFailed)
53    }
54}
55
56/// Sets the name of the console window.
57pub fn set_console_title(title: &str) -> Result<(), Error> {
58    crossterm::execute!(std::io::stdout(), crossterm::terminal::SetTitle(title))
59        .map_err(Error::VtOperationFailed)
60}
61
62/// Clones `file` into a `File`.
63///
64/// # Safety
65/// The caller must ensure `file` owns a valid file.
66#[cfg(windows)]
67fn clone_file(file: impl std::os::windows::io::AsHandle) -> std::fs::File {
68    file.as_handle().try_clone_to_owned().unwrap().into()
69}
70
71/// Clones `file` into a `File`.
72///
73/// # Safety
74/// The caller must ensure `file` owns a valid file.
75#[cfg(unix)]
76fn clone_file(file: impl std::os::unix::io::AsFd) -> std::fs::File {
77    file.as_fd().try_clone_to_owned().unwrap().into()
78}
79
80/// Returns a non-buffering stdout, with no special console handling on Windows.
81pub fn raw_stdout() -> std::fs::File {
82    clone_file(std::io::stdout())
83}
84
85/// Returns a non-buffering stderr, with no special console handling on Windows.
86pub fn raw_stderr() -> std::fs::File {
87    clone_file(std::io::stderr())
88}
89
90/// Sets a panic handler to restore the terminal state when the process panics.
91#[cfg(unix)]
92pub fn revert_terminal_on_panic() {
93    let orig_termios = get_termios();
94
95    let base_hook = std::panic::take_hook();
96    std::panic::set_hook(Box::new(move |info| {
97        eprintln!("restoring terminal attributes on panic...");
98        set_termios(orig_termios);
99        base_hook(info)
100    }));
101}
102
103/// Opaque wrapper around `libc::termios`.
104#[cfg(unix)]
105#[derive(Copy, Clone)]
106pub struct Termios(libc::termios);
107
108/// Get the current termios settings for stderr.
109#[cfg(unix)]
110pub fn get_termios() -> Termios {
111    let mut orig_termios = std::mem::MaybeUninit::<libc::termios>::uninit();
112    // SAFETY: `tcgetattr` has no preconditions, and stderr has been checked to be a tty
113    let ret = unsafe { libc::tcgetattr(libc::STDERR_FILENO, orig_termios.as_mut_ptr()) };
114    if ret != 0 {
115        panic!(
116            "error: could not save term attributes: {}",
117            std::io::Error::last_os_error()
118        );
119    }
120    // SAFETY: `tcgetattr` returned successfully, therefore `orig_termios` has been initialized
121    let orig_termios = unsafe { orig_termios.assume_init() };
122    Termios(orig_termios)
123}
124
125/// Set the termios settings for stderr.
126#[cfg(unix)]
127pub fn set_termios(termios: Termios) {
128    // SAFETY: stderr is guaranteed to be an open fd, and `termios` is a valid termios struct.
129    let ret = unsafe { libc::tcsetattr(libc::STDERR_FILENO, libc::TCSAFLUSH, &termios.0) };
130    if ret != 0 {
131        panic!(
132            "error: could not restore term attributes via tcsetattr: {}",
133            std::io::Error::last_os_error()
134        );
135    }
136}