1use std::fmt;
29use std::fs::{File, OpenOptions};
30use std::io::{self, Read, Write};
31use std::sync::{Mutex, OnceLock};
32
33#[cfg(unix)]
34use std::os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd};
35
36#[cfg(windows)]
37use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle, RawHandle};
38
39static INPUT: OnceLock<io::Result<Mutex<File>>> = OnceLock::new();
52static OUTPUT: OnceLock<io::Result<Mutex<File>>> = OnceLock::new();
53
54fn input_lock() -> io::Result<&'static Mutex<File>> {
55 cached(INPUT.get_or_init(open_input))
56}
57
58fn output_lock() -> io::Result<&'static Mutex<File>> {
59 cached(OUTPUT.get_or_init(open_output))
60}
61
62fn cached(slot: &'static io::Result<Mutex<File>>) -> io::Result<&'static Mutex<File>> {
63 match slot {
64 Ok(m) => Ok(m),
65 Err(e) => Err(io::Error::new(e.kind(), e.to_string())),
69 }
70}
71
72#[cfg(unix)]
73fn open_input() -> io::Result<Mutex<File>> {
74 OpenOptions::new()
75 .read(true)
76 .write(true)
77 .open("/dev/tty")
78 .map(Mutex::new)
79}
80
81#[cfg(unix)]
82fn open_output() -> io::Result<Mutex<File>> {
83 let file = OpenOptions::new().read(true).write(true).open("/dev/tty")?;
87 Ok(Mutex::new(file))
88}
89
90#[cfg(windows)]
91fn open_input() -> io::Result<Mutex<File>> {
92 OpenOptions::new()
93 .read(true)
94 .write(true)
95 .open("CONIN$")
96 .map(Mutex::new)
97}
98
99#[cfg(windows)]
100fn open_output() -> io::Result<Mutex<File>> {
101 OpenOptions::new()
102 .read(true)
103 .write(true)
104 .open("CONOUT$")
105 .map(Mutex::new)
106}
107
108#[cfg(not(any(unix, windows)))]
109fn open_input() -> io::Result<Mutex<File>> {
110 Err(io::Error::new(
111 io::ErrorKind::Unsupported,
112 "open_tty is not available on this platform",
113 ))
114}
115
116#[cfg(not(any(unix, windows)))]
117fn open_output() -> io::Result<Mutex<File>> {
118 open_input()
119}
120
121pub fn open_tty() -> io::Result<(TtyInput, TtyOutput)> {
145 Ok((
146 TtyInput {
147 inner: input_lock()?,
148 },
149 TtyOutput {
150 inner: output_lock()?,
151 },
152 ))
153}
154
155#[derive(Clone, Copy)]
165pub struct TtyInput {
166 inner: &'static Mutex<File>,
167}
168
169#[derive(Clone, Copy)]
184pub struct TtyOutput {
185 inner: &'static Mutex<File>,
186}
187
188impl fmt::Debug for TtyInput {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 f.debug_struct("TtyInput").finish()
191 }
192}
193
194impl fmt::Debug for TtyOutput {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 f.debug_struct("TtyOutput").finish()
197 }
198}
199
200impl Read for TtyInput {
201 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
202 (&*self).read(buf)
203 }
204}
205
206impl Read for &TtyInput {
207 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
208 let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
209 (&*guard).read(buf)
210 }
211}
212
213impl Write for TtyOutput {
214 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
215 (&*self).write(buf)
216 }
217
218 fn flush(&mut self) -> io::Result<()> {
219 (&*self).flush()
220 }
221}
222
223impl Write for &TtyOutput {
224 #[cfg(windows)]
225 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
226 let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
227 write_to_console_or_file(&guard, buf)
228 }
229
230 #[cfg(not(windows))]
231 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
232 let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
233 (&*guard).write(buf)
234 }
235
236 fn flush(&mut self) -> io::Result<()> {
237 let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
238 (&*guard).flush()
239 }
240}
241
242#[cfg(unix)]
243impl AsFd for TtyInput {
244 fn as_fd(&self) -> BorrowedFd<'_> {
245 let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
246 unsafe { BorrowedFd::borrow_raw(guard.as_raw_fd()) }
250 }
251}
252
253#[cfg(unix)]
254impl AsRawFd for TtyInput {
255 fn as_raw_fd(&self) -> RawFd {
256 let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
257 guard.as_raw_fd()
258 }
259}
260
261#[cfg(unix)]
262impl AsFd for TtyOutput {
263 fn as_fd(&self) -> BorrowedFd<'_> {
264 let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
265 unsafe { BorrowedFd::borrow_raw(guard.as_raw_fd()) }
267 }
268}
269
270#[cfg(unix)]
271impl AsRawFd for TtyOutput {
272 fn as_raw_fd(&self) -> RawFd {
273 let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
274 guard.as_raw_fd()
275 }
276}
277
278#[cfg(windows)]
279impl AsHandle for TtyInput {
280 fn as_handle(&self) -> BorrowedHandle<'_> {
281 let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
282 unsafe { BorrowedHandle::borrow_raw(guard.as_raw_handle() as _) }
286 }
287}
288
289#[cfg(windows)]
290impl AsRawHandle for TtyInput {
291 fn as_raw_handle(&self) -> RawHandle {
292 let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
293 guard.as_raw_handle()
294 }
295}
296
297#[cfg(windows)]
298impl AsHandle for TtyOutput {
299 fn as_handle(&self) -> BorrowedHandle<'_> {
300 let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
301 unsafe { BorrowedHandle::borrow_raw(guard.as_raw_handle() as _) }
303 }
304}
305
306#[cfg(windows)]
307impl AsRawHandle for TtyOutput {
308 fn as_raw_handle(&self) -> RawHandle {
309 let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner());
310 guard.as_raw_handle()
311 }
312}
313
314#[cfg(windows)]
315fn is_console(h: RawHandle) -> bool {
316 use windows_sys::Win32::Foundation::HANDLE;
317 use windows_sys::Win32::System::Console::GetConsoleMode;
318 let mut mode: u32 = 0;
319 unsafe { GetConsoleMode(h as HANDLE, &mut mode) != 0 }
320}
321
322#[cfg(windows)]
323fn write_to_console_or_file(file: &File, buf: &[u8]) -> io::Result<usize> {
324 if !is_console(file.as_raw_handle()) {
325 return (&*file).write(buf);
326 }
327
328 let utf8 = match std::str::from_utf8(buf) {
333 Ok(s) => s,
334 Err(e) => {
335 let valid = e.valid_up_to();
336 if valid == 0 {
337 return write_utf16_console(file.as_raw_handle(), &['\u{FFFD}' as u16]).map(|_| 1);
341 }
342 unsafe { std::str::from_utf8_unchecked(&buf[..valid]) }
345 }
346 };
347
348 let utf16: Vec<u16> = utf8.encode_utf16().collect();
349 if utf16.is_empty() {
350 return Ok(0);
351 }
352 let units = write_utf16_console(file.as_raw_handle(), &utf16)?;
353
354 let mut consumed_units = 0usize;
357 let mut consumed_bytes = 0usize;
358 for c in utf8.chars() {
359 let cu = c.len_utf16();
360 if consumed_units + cu > units {
361 break;
362 }
363 consumed_units += cu;
364 consumed_bytes += c.len_utf8();
365 if consumed_units == units {
366 break;
367 }
368 }
369 Ok(consumed_bytes)
370}
371
372#[cfg(windows)]
373fn write_utf16_console(h: RawHandle, data: &[u16]) -> io::Result<usize> {
374 use windows_sys::Win32::Foundation::HANDLE;
375 use windows_sys::Win32::System::Console::WriteConsoleW;
376 let mut written: u32 = 0;
377 let ok = unsafe {
378 WriteConsoleW(
379 h as HANDLE,
380 data.as_ptr(),
381 data.len() as u32,
382 &mut written,
383 std::ptr::null(),
384 )
385 };
386 if ok == 0 {
387 return Err(io::Error::last_os_error());
388 }
389 Ok(written as usize)
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn open_tty_returns_either_pair_or_error() {
398 match open_tty() {
403 Ok((input, _output)) => {
404 #[cfg(unix)]
405 {
406 assert!(input.as_raw_fd() >= 0);
407 }
408 #[cfg(windows)]
409 {
410 let _ = input.as_raw_handle();
411 }
412 #[cfg(not(any(unix, windows)))]
413 {
414 let _ = input;
415 }
416 }
417 Err(_e) => {
418 }
426 }
427 }
428
429 #[test]
430 fn repeated_calls_share_underlying_handle() {
431 let Ok((input_a, output_a)) = open_tty() else {
432 return;
433 };
434 let Ok((input_b, output_b)) = open_tty() else {
435 unreachable!("second open_tty must succeed when the first did");
436 };
437
438 #[cfg(unix)]
441 {
442 assert_eq!(input_a.as_raw_fd(), input_b.as_raw_fd());
443 assert_eq!(output_a.as_raw_fd(), output_b.as_raw_fd());
444 }
445 #[cfg(windows)]
446 {
447 assert_eq!(input_a.as_raw_handle(), input_b.as_raw_handle());
448 assert_eq!(output_a.as_raw_handle(), output_b.as_raw_handle());
449 }
450 }
451}