terminal_trx/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! Provides a handle to the terminal of the current process that is both readable and writable.
4//!
5//! ## Example: Read and Write
6//!```no_run
7//! use terminal_trx::terminal;
8//! use std::io::{BufReader, BufRead as _, Write as _};
9//!
10//! let mut terminal = terminal().unwrap();
11//!
12//! write!(terminal, "hello world").unwrap();
13//!
14//! let mut reader = BufReader::new(&mut terminal);
15//! let mut line = String::new();
16//! reader.read_line(&mut line).unwrap();
17//! ```
18//!
19//! ## Example: Enable Raw Mode
20//! ```no_run
21//! use terminal_trx::terminal;
22//!
23//! let mut tty = terminal().unwrap();
24//! let mut lock = tty.lock();
25//! let mut raw_mode = lock.enable_raw_mode().unwrap();
26//!
27//! // You can now perform read and write operations using `raw_mode`.
28//! ```
29
30use cfg_if::cfg_if;
31use std::io;
32use std::marker::PhantomData;
33use std::sync::{Mutex, MutexGuard};
34
35cfg_if! {
36    if #[cfg(all(unix, not(terminal_trx_test_unsupported)))] {
37        mod unix;
38        use unix as imp;
39    } else if #[cfg(all(windows, not(terminal_trx_test_unsupported)))] {
40        mod windows;
41        use windows as imp;
42    } else {
43        mod unsupported;
44        use unsupported as imp;
45    }
46}
47
48#[doc = include_str!("../readme.md")]
49#[cfg(doctest)]
50pub mod readme_doctests {}
51
52static TERMINAL_LOCK: Mutex<()> = Mutex::new(());
53
54/// Creates a readable and writable handle to the terminal (or TTY) if available.
55///
56/// Use [`Terminal::lock`] if you want to avoid locking before each read / write call.
57///
58/// ## Unix
59/// On Unix, the terminal is retrieved by successively testing
60/// * the standard error,
61/// * standard input,
62/// * standard output,
63/// * and finally `/dev/tty`.
64///
65/// ## Windows
66/// On Windows, the reading half is retrieved by first testing the standard input, falling back to `CONIN$`. \
67/// The writing half is retrieved by successfully testing
68/// * the standard error,
69/// * standard output,
70/// * and finally `CONOUT$`.
71pub fn terminal() -> io::Result<Terminal> {
72    imp::terminal().map(Terminal)
73}
74
75macro_rules! impl_transceive {
76    ($($extra_supertraits:tt)*) => {
77        /// A trait for objects that are both [`io::Read`] and [`io::Write`].
78        pub trait Transceive: io::Read + io::Write $($extra_supertraits)* + sealed::Sealed {}
79    };
80}
81
82cfg_if! {
83    if #[cfg(terminal_trx_test_unsupported)] {
84        impl_transceive! { }
85    } else if #[cfg(unix)] {
86        impl_transceive! { + std::os::fd::AsFd + std::os::fd::AsRawFd }
87    } else if #[cfg(windows)] {
88        impl_transceive! { + ConsoleHandles }
89    } else {
90        impl_transceive! { }
91    }
92}
93
94/// A trait to borrow the console handles from the underlying console.
95#[cfg(all(windows, not(terminal_trx_test_unsupported)))]
96#[cfg_attr(docsrs, doc(cfg(windows)))]
97pub trait ConsoleHandles {
98    /// Returns a handle to the consoles's input buffer `CONIN$`.
99    fn input_buffer_handle(&self) -> std::os::windows::io::BorrowedHandle<'_>;
100
101    /// Returns a handle to the consoles's screen buffer `CONOUT$`.
102    fn screen_buffer_handle(&self) -> std::os::windows::io::BorrowedHandle<'_>;
103}
104
105mod sealed {
106    pub trait Sealed {}
107}
108
109/// A readable and writable handle to the terminal (or TTY), created using [`terminal()`].
110/// You can read and write data using the [`io::Read`] and [`io::Write`] implementations respectively.
111///
112/// Use [`Terminal::lock`] if you want to avoid locking before each read / write call.
113#[derive(Debug)]
114pub struct Terminal(imp::Terminal);
115
116#[cfg(test)]
117static_assertions::assert_impl_all!(Terminal: Send, Sync, std::panic::UnwindSafe, std::panic::RefUnwindSafe);
118
119impl sealed::Sealed for Terminal {}
120impl Transceive for Terminal {}
121
122impl io::Read for Terminal {
123    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
124        self.lock().read(buf)
125    }
126}
127
128impl io::Write for Terminal {
129    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
130        self.lock().write(buf)
131    }
132
133    fn flush(&mut self) -> io::Result<()> {
134        self.lock().flush()
135    }
136}
137
138impl Terminal {
139    /// Locks access to this terminal, returing a guard that is readable and writable.
140    ///
141    /// Until the returned [`TerminalLock`] is dropped, all standard I/O streams
142    /// that refer to the same terminal will be locked.
143    pub fn lock(&mut self) -> TerminalLock<'_> {
144        let mutex_guard = TERMINAL_LOCK.lock().unwrap_or_else(|e| e.into_inner());
145        let stdio_locks = self.0.lock_stdio();
146        TerminalLock {
147            inner: &mut self.0,
148            _stdio_locks: stdio_locks,
149            _mutex_guard: mutex_guard,
150            _phantom_data: PhantomData,
151        }
152    }
153
154    /// Tests if at least one of the standard I/O stream (stdin, stdout, stderr) is connected to this terminal.
155    pub fn has_connected_stdio_stream(&self) -> bool {
156        self.0.has_connected_stdio_stream()
157    }
158}
159
160/// Guard for exclusive read- and write access to the terminal.
161/// Can be created using [`Terminal::lock`].
162#[derive(Debug)]
163pub struct TerminalLock<'a> {
164    inner: &'a mut imp::Terminal,
165    _mutex_guard: MutexGuard<'static, ()>,
166    _stdio_locks: StdioLocks,
167    _phantom_data: PhantomData<*mut ()>,
168}
169
170#[cfg(test)]
171static_assertions::assert_not_impl_any!(TerminalLock<'_>: Send, Sync);
172
173impl TerminalLock<'_> {
174    /// Enables raw mode on this terminal for the lifetime of the returned guard.
175    ///
176    /// Raw mode has two effects:
177    /// * Input typed into the terminal is not visible.
178    /// * Input is can be read immediately (usually input is only available after a newline character).
179    /// * (Windows) Ensures that VT sequences are processed in both input and output.
180    ///
181    /// ### Windows
182    /// This function returns an [`Err`] with [`ErrorKind::Unsupported`](`io::ErrorKind::Unsupported`) if the standard input is
183    /// connected to a MSYS/Cygwin terminal.
184    pub fn enable_raw_mode(&mut self) -> io::Result<RawModeGuard<'_>> {
185        self.inner.enable_raw_mode().map(RawModeGuard)
186    }
187}
188
189impl sealed::Sealed for TerminalLock<'_> {}
190impl Transceive for TerminalLock<'_> {}
191
192impl io::Read for TerminalLock<'_> {
193    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
194        self.inner.read(buf)
195    }
196}
197
198impl io::Write for TerminalLock<'_> {
199    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
200        self.inner.write(buf)
201    }
202
203    fn flush(&mut self) -> io::Result<()> {
204        self.inner.flush()
205    }
206}
207
208#[derive(Debug)]
209struct StdioLocks {
210    #[allow(dead_code)]
211    stdin_lock: Option<io::StdinLock<'static>>,
212    #[allow(dead_code)]
213    stdout_lock: Option<io::StdoutLock<'static>>,
214    #[allow(dead_code)]
215    stderr_lock: Option<io::StderrLock<'static>>,
216}
217
218/// Guard for raw mode on the terminal, disables raw mode on drop.
219/// Can be crated using [`TerminalLock::enable_raw_mode`].
220#[derive(Debug)]
221pub struct RawModeGuard<'a>(imp::RawModeGuard<'a>);
222
223impl sealed::Sealed for RawModeGuard<'_> {}
224impl Transceive for RawModeGuard<'_> {}
225
226impl io::Read for RawModeGuard<'_> {
227    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
228        self.0.read(buf)
229    }
230}
231
232impl io::Write for RawModeGuard<'_> {
233    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
234        self.0.write(buf)
235    }
236
237    fn flush(&mut self) -> io::Result<()> {
238        self.0.flush()
239    }
240}