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