console/
term.rs

1use alloc::sync::Arc;
2use core::fmt::{Debug, Display};
3use std::io::{self, Read, Write};
4#[cfg(any(unix, all(target_os = "wasi", target_env = "p1")))]
5use std::os::fd::{AsRawFd, RawFd};
6#[cfg(windows)]
7use std::os::windows::io::{AsRawHandle, RawHandle};
8use std::sync::{Mutex, RwLock};
9
10use crate::{kb::Key, utils::Style};
11
12#[cfg(unix)]
13trait TermWrite: Write + Debug + AsRawFd + Send {}
14#[cfg(unix)]
15impl<T: Write + Debug + AsRawFd + Send> TermWrite for T {}
16
17#[cfg(unix)]
18trait TermRead: Read + Debug + AsRawFd + Send {}
19#[cfg(unix)]
20impl<T: Read + Debug + AsRawFd + Send> TermRead for T {}
21
22#[cfg(unix)]
23#[derive(Debug, Clone)]
24pub struct ReadWritePair {
25    #[allow(unused)]
26    read: Arc<Mutex<dyn TermRead>>,
27    write: Arc<Mutex<dyn TermWrite>>,
28    style: Style,
29}
30
31/// Where the term is writing.
32#[derive(Debug, Clone)]
33pub enum TermTarget {
34    Stdout,
35    Stderr,
36    #[cfg(unix)]
37    ReadWritePair(ReadWritePair),
38}
39
40#[derive(Debug)]
41struct TermInner {
42    target: TermTarget,
43    buffer: Option<Mutex<Vec<u8>>>,
44    prompt: RwLock<String>,
45    prompt_guard: Mutex<()>,
46}
47
48/// The family of the terminal.
49#[derive(Debug, Copy, Clone, PartialEq, Eq)]
50pub enum TermFamily {
51    /// Redirected to a file or file like thing.
52    File,
53    /// A standard unix terminal.
54    UnixTerm,
55    /// A cmd.exe like windows console.
56    WindowsConsole,
57    /// A dummy terminal (for instance on wasm)
58    Dummy,
59}
60
61/// Gives access to the terminal features.
62#[derive(Debug, Clone)]
63pub struct TermFeatures<'a>(&'a Term);
64
65impl TermFeatures<'_> {
66    /// Check if this is a real user attended terminal (`isatty`)
67    #[inline]
68    pub fn is_attended(&self) -> bool {
69        is_a_terminal(self.0)
70    }
71
72    /// Check if colors are supported by this terminal.
73    ///
74    /// This does not check if colors are enabled.  Currently all terminals
75    /// are considered to support colors
76    #[inline]
77    pub fn colors_supported(&self) -> bool {
78        is_a_color_terminal(self.0)
79    }
80
81    /// Check if true colors are supported by this terminal.
82    pub fn true_colors_supported(&self) -> bool {
83        is_a_true_color_terminal(self.0)
84    }
85
86    /// Check if this terminal is an msys terminal.
87    ///
88    /// This is sometimes useful to disable features that are known to not
89    /// work on msys terminals or require special handling.
90    #[inline]
91    pub fn is_msys_tty(&self) -> bool {
92        #[cfg(windows)]
93        {
94            msys_tty_on(self.0)
95        }
96        #[cfg(not(windows))]
97        {
98            false
99        }
100    }
101
102    /// Check if this terminal wants emojis.
103    #[inline]
104    pub fn wants_emoji(&self) -> bool {
105        self.is_attended() && wants_emoji()
106    }
107
108    /// Return the family of the terminal.
109    #[inline]
110    pub fn family(&self) -> TermFamily {
111        if !self.is_attended() {
112            return TermFamily::File;
113        }
114        #[cfg(windows)]
115        {
116            TermFamily::WindowsConsole
117        }
118        #[cfg(all(unix, not(target_arch = "wasm32")))]
119        {
120            TermFamily::UnixTerm
121        }
122        #[cfg(target_arch = "wasm32")]
123        {
124            TermFamily::Dummy
125        }
126    }
127}
128
129/// Abstraction around a terminal.
130///
131/// A terminal can be cloned.  If a buffer is used it's shared across all
132/// clones which means it largely acts as a handle.
133#[derive(Clone, Debug)]
134pub struct Term {
135    inner: Arc<TermInner>,
136    pub(crate) is_msys_tty: bool,
137    pub(crate) is_tty: bool,
138}
139
140impl Term {
141    fn with_inner(inner: TermInner) -> Term {
142        let mut term = Term {
143            inner: Arc::new(inner),
144            is_msys_tty: false,
145            is_tty: false,
146        };
147
148        term.is_msys_tty = term.features().is_msys_tty();
149        term.is_tty = term.features().is_attended();
150        term
151    }
152
153    /// Return a new unbuffered terminal.
154    #[inline]
155    pub fn stdout() -> Term {
156        Term::with_inner(TermInner {
157            target: TermTarget::Stdout,
158            buffer: None,
159            prompt: RwLock::new(String::new()),
160            prompt_guard: Mutex::new(()),
161        })
162    }
163
164    /// Return a new unbuffered terminal to stderr.
165    #[inline]
166    pub fn stderr() -> Term {
167        Term::with_inner(TermInner {
168            target: TermTarget::Stderr,
169            buffer: None,
170            prompt: RwLock::new(String::new()),
171            prompt_guard: Mutex::new(()),
172        })
173    }
174
175    /// Return a new buffered terminal.
176    pub fn buffered_stdout() -> Term {
177        Term::with_inner(TermInner {
178            target: TermTarget::Stdout,
179            buffer: Some(Mutex::new(vec![])),
180            prompt: RwLock::new(String::new()),
181            prompt_guard: Mutex::new(()),
182        })
183    }
184
185    /// Return a new buffered terminal to stderr.
186    pub fn buffered_stderr() -> Term {
187        Term::with_inner(TermInner {
188            target: TermTarget::Stderr,
189            buffer: Some(Mutex::new(vec![])),
190            prompt: RwLock::new(String::new()),
191            prompt_guard: Mutex::new(()),
192        })
193    }
194
195    /// Return a terminal for the given Read/Write pair styled like stderr.
196    #[cfg(unix)]
197    pub fn read_write_pair<R, W>(read: R, write: W) -> Term
198    where
199        R: Read + Debug + AsRawFd + Send + 'static,
200        W: Write + Debug + AsRawFd + Send + 'static,
201    {
202        Self::read_write_pair_with_style(read, write, Style::new().for_stderr())
203    }
204
205    /// Return a terminal for the given Read/Write pair.
206    #[cfg(unix)]
207    pub fn read_write_pair_with_style<R, W>(read: R, write: W, style: Style) -> Term
208    where
209        R: Read + Debug + AsRawFd + Send + 'static,
210        W: Write + Debug + AsRawFd + Send + 'static,
211    {
212        Term::with_inner(TermInner {
213            target: TermTarget::ReadWritePair(ReadWritePair {
214                read: Arc::new(Mutex::new(read)),
215                write: Arc::new(Mutex::new(write)),
216                style,
217            }),
218            buffer: None,
219            prompt: RwLock::new(String::new()),
220            prompt_guard: Mutex::new(()),
221        })
222    }
223
224    /// Return the style for this terminal.
225    #[inline]
226    pub fn style(&self) -> Style {
227        match self.inner.target {
228            TermTarget::Stderr => Style::new().for_stderr(),
229            TermTarget::Stdout => Style::new().for_stdout(),
230            #[cfg(unix)]
231            TermTarget::ReadWritePair(ReadWritePair { ref style, .. }) => style.clone(),
232        }
233    }
234
235    /// Return the target of this terminal.
236    #[inline]
237    pub fn target(&self) -> TermTarget {
238        self.inner.target.clone()
239    }
240
241    #[doc(hidden)]
242    pub fn write_str(&self, s: &str) -> io::Result<()> {
243        match self.inner.buffer {
244            Some(ref buffer) => buffer.lock().unwrap().write_all(s.as_bytes()),
245            None => self.write_through(s.as_bytes()),
246        }
247    }
248
249    /// Write a string to the terminal and add a newline.
250    pub fn write_line(&self, s: &str) -> io::Result<()> {
251        let prompt = self.inner.prompt.read().unwrap();
252        if !prompt.is_empty() {
253            self.clear_line()?;
254        }
255        match self.inner.buffer {
256            Some(ref mutex) => {
257                let mut buffer = mutex.lock().unwrap();
258                buffer.extend_from_slice(s.as_bytes());
259                buffer.push(b'\n');
260                buffer.extend_from_slice(prompt.as_bytes());
261                Ok(())
262            }
263            None => self.write_through(format!("{}\n{}", s, prompt.as_str()).as_bytes()),
264        }
265    }
266
267    /// Read a single character from the terminal.
268    ///
269    /// This does not echo the character and blocks until a single character
270    /// or complete key chord is entered.  If the terminal is not user attended
271    /// the return value will be an error.
272    pub fn read_char(&self) -> io::Result<char> {
273        if !self.is_tty {
274            return Err(io::Error::new(
275                io::ErrorKind::NotConnected,
276                "Not a terminal",
277            ));
278        }
279        loop {
280            match self.read_key()? {
281                Key::Char(c) => {
282                    return Ok(c);
283                }
284                Key::Enter => {
285                    return Ok('\n');
286                }
287                _ => {}
288            }
289        }
290    }
291
292    /// Read a single key from the terminal.
293    ///
294    /// This does not echo anything.  If the terminal is not user attended
295    /// the return value will always be the unknown key.
296    pub fn read_key(&self) -> io::Result<Key> {
297        if !self.is_tty {
298            Ok(Key::Unknown)
299        } else {
300            read_single_key(false)
301        }
302    }
303
304    pub fn read_key_raw(&self) -> io::Result<Key> {
305        if !self.is_tty {
306            Ok(Key::Unknown)
307        } else {
308            read_single_key(true)
309        }
310    }
311
312    /// Read one line of input.
313    ///
314    /// This does not include the trailing newline.  If the terminal is not
315    /// user attended the return value will always be an empty string.
316    pub fn read_line(&self) -> io::Result<String> {
317        self.read_line_initial_text("")
318    }
319
320    /// Read one line of input with initial text.
321    ///
322    /// This method blocks until no other thread is waiting for this read_line
323    /// before reading a line from the terminal.
324    /// This does not include the trailing newline.  If the terminal is not
325    /// user attended the return value will always be an empty string.
326    pub fn read_line_initial_text(&self, initial: &str) -> io::Result<String> {
327        if !self.is_tty {
328            return Ok("".into());
329        }
330        *self.inner.prompt.write().unwrap() = initial.to_string();
331        // use a guard in order to prevent races with other calls to read_line_initial_text
332        let _guard = self.inner.prompt_guard.lock().unwrap();
333
334        self.write_str(initial)?;
335
336        fn read_line_internal(slf: &Term, initial: &str) -> io::Result<String> {
337            let prefix_len = initial.len();
338
339            let mut chars: Vec<char> = initial.chars().collect();
340
341            loop {
342                match slf.read_key()? {
343                    Key::Backspace => {
344                        if prefix_len < chars.len() {
345                            if let Some(ch) = chars.pop() {
346                                slf.clear_chars(crate::utils::char_width(ch))?;
347                            }
348                        }
349                        slf.flush()?;
350                    }
351                    Key::Char(chr) => {
352                        chars.push(chr);
353                        let mut bytes_char = [0; 4];
354                        chr.encode_utf8(&mut bytes_char);
355                        slf.write_str(chr.encode_utf8(&mut bytes_char))?;
356                        slf.flush()?;
357                    }
358                    Key::Enter => {
359                        slf.write_through(format!("\n{initial}").as_bytes())?;
360                        break;
361                    }
362                    _ => (),
363                }
364            }
365            Ok(chars.iter().skip(prefix_len).collect::<String>())
366        }
367        let ret = read_line_internal(self, initial);
368
369        *self.inner.prompt.write().unwrap() = String::new();
370        ret
371    }
372
373    /// Read a line of input securely.
374    ///
375    /// This is similar to `read_line` but will not echo the output.  This
376    /// also switches the terminal into a different mode where not all
377    /// characters might be accepted.
378    pub fn read_secure_line(&self) -> io::Result<String> {
379        if !self.is_tty {
380            return Ok("".into());
381        }
382        match read_secure() {
383            Ok(rv) => {
384                self.write_line("")?;
385                Ok(rv)
386            }
387            Err(err) => Err(err),
388        }
389    }
390
391    /// Flush internal buffers.
392    ///
393    /// This forces the contents of the internal buffer to be written to
394    /// the terminal.  This is unnecessary for unbuffered terminals which
395    /// will automatically flush.
396    pub fn flush(&self) -> io::Result<()> {
397        if let Some(ref buffer) = self.inner.buffer {
398            let mut buffer = buffer.lock().unwrap();
399            if !buffer.is_empty() {
400                self.write_through(&buffer[..])?;
401                buffer.clear();
402            }
403        }
404        Ok(())
405    }
406
407    /// Check if the terminal is indeed a terminal.
408    #[inline]
409    pub fn is_term(&self) -> bool {
410        self.is_tty
411    }
412
413    /// Check for common terminal features.
414    #[inline]
415    pub fn features(&self) -> TermFeatures<'_> {
416        TermFeatures(self)
417    }
418
419    /// Return the terminal size in rows and columns or gets sensible defaults.
420    #[inline]
421    pub fn size(&self) -> (u16, u16) {
422        self.size_checked().unwrap_or((24, DEFAULT_WIDTH))
423    }
424
425    /// Return the terminal size in rows and columns.
426    ///
427    /// If the size cannot be reliably determined `None` is returned.
428    #[inline]
429    pub fn size_checked(&self) -> Option<(u16, u16)> {
430        terminal_size(self)
431    }
432
433    /// Move the cursor to row `x` and column `y`. Values are 0-based.
434    #[inline]
435    pub fn move_cursor_to(&self, x: usize, y: usize) -> io::Result<()> {
436        move_cursor_to(self, x, y)
437    }
438
439    /// Move the cursor up by `n` lines, if possible.
440    ///
441    /// If there are less than `n` lines above the current cursor position,
442    /// the cursor is moved to the top line of the terminal (i.e., as far up as possible).
443    #[inline]
444    pub fn move_cursor_up(&self, n: usize) -> io::Result<()> {
445        move_cursor_up(self, n)
446    }
447
448    /// Move the cursor down by `n` lines, if possible.
449    ///
450    /// If there are less than `n` lines below the current cursor position,
451    /// the cursor is moved to the bottom line of the terminal (i.e., as far down as possible).
452    #[inline]
453    pub fn move_cursor_down(&self, n: usize) -> io::Result<()> {
454        move_cursor_down(self, n)
455    }
456
457    /// Move the cursor `n` characters to the left, if possible.
458    ///
459    /// If there are fewer than `n` characters to the left of the current cursor position,
460    /// the cursor is moved to the beginning of the line (i.e., as far to the left as possible).
461    #[inline]
462    pub fn move_cursor_left(&self, n: usize) -> io::Result<()> {
463        move_cursor_left(self, n)
464    }
465
466    /// Move the cursor `n` characters to the right.
467    ///
468    /// If there are fewer than `n` characters to the right of the current cursor position,
469    /// the cursor is moved to the end of the current line (i.e., as far to the right as possible).
470    #[inline]
471    pub fn move_cursor_right(&self, n: usize) -> io::Result<()> {
472        move_cursor_right(self, n)
473    }
474
475    /// Clear the current line.
476    ///
477    /// Position the cursor at the beginning of the current line.
478    #[inline]
479    pub fn clear_line(&self) -> io::Result<()> {
480        clear_line(self)
481    }
482
483    /// Clear the last `n` lines before the current line.
484    ///
485    /// Position the cursor at the beginning of the first line that was cleared.
486    pub fn clear_last_lines(&self, n: usize) -> io::Result<()> {
487        self.move_cursor_up(n)?;
488        for _ in 0..n {
489            self.clear_line()?;
490            self.move_cursor_down(1)?;
491        }
492        self.move_cursor_up(n)?;
493        Ok(())
494    }
495
496    /// Clear the entire screen.
497    ///
498    /// Move the cursor to the upper left corner of the screen.
499    #[inline]
500    pub fn clear_screen(&self) -> io::Result<()> {
501        clear_screen(self)
502    }
503
504    /// Clear everything from the current cursor position to the end of the screen.
505    /// The cursor stays in its position.
506    #[inline]
507    pub fn clear_to_end_of_screen(&self) -> io::Result<()> {
508        clear_to_end_of_screen(self)
509    }
510
511    /// Clear the last `n` characters of the current line.
512    #[inline]
513    pub fn clear_chars(&self, n: usize) -> io::Result<()> {
514        clear_chars(self, n)
515    }
516
517    /// Set the terminal title.
518    pub fn set_title<T: Display>(&self, title: T) {
519        if !self.is_tty {
520            return;
521        }
522        set_title(title);
523    }
524
525    /// Make the cursor visible again.
526    #[inline]
527    pub fn show_cursor(&self) -> io::Result<()> {
528        show_cursor(self)
529    }
530
531    /// Hide the cursor.
532    #[inline]
533    pub fn hide_cursor(&self) -> io::Result<()> {
534        hide_cursor(self)
535    }
536
537    // helpers
538
539    #[cfg(all(windows, feature = "windows-console-colors"))]
540    fn write_through(&self, bytes: &[u8]) -> io::Result<()> {
541        if self.is_msys_tty || !self.is_tty {
542            self.write_through_common(bytes)
543        } else {
544            match self.inner.target {
545                TermTarget::Stdout => console_colors(self, Console::stdout()?, bytes),
546                TermTarget::Stderr => console_colors(self, Console::stderr()?, bytes),
547            }
548        }
549    }
550
551    #[cfg(not(all(windows, feature = "windows-console-colors")))]
552    fn write_through(&self, bytes: &[u8]) -> io::Result<()> {
553        self.write_through_common(bytes)
554    }
555
556    pub(crate) fn write_through_common(&self, bytes: &[u8]) -> io::Result<()> {
557        match self.inner.target {
558            TermTarget::Stdout => {
559                io::stdout().write_all(bytes)?;
560                io::stdout().flush()?;
561            }
562            TermTarget::Stderr => {
563                io::stderr().write_all(bytes)?;
564                io::stderr().flush()?;
565            }
566            #[cfg(unix)]
567            TermTarget::ReadWritePair(ReadWritePair { ref write, .. }) => {
568                let mut write = write.lock().unwrap();
569                write.write_all(bytes)?;
570                write.flush()?;
571            }
572        }
573        Ok(())
574    }
575}
576
577/// A fast way to check if the application has a user attended for stdout.
578///
579/// This means that stdout is connected to a terminal instead of a
580/// file or redirected by other means. This is a shortcut for
581/// checking the `is_attended` feature on the stdout terminal.
582#[inline]
583pub fn user_attended() -> bool {
584    Term::stdout().features().is_attended()
585}
586
587/// A fast way to check if the application has a user attended for stderr.
588///
589/// This means that stderr is connected to a terminal instead of a
590/// file or redirected by other means. This is a shortcut for
591/// checking the `is_attended` feature on the stderr terminal.
592#[inline]
593pub fn user_attended_stderr() -> bool {
594    Term::stderr().features().is_attended()
595}
596
597#[cfg(any(unix, all(target_os = "wasi", target_env = "p1")))]
598impl AsRawFd for Term {
599    fn as_raw_fd(&self) -> RawFd {
600        match self.inner.target {
601            TermTarget::Stdout => libc::STDOUT_FILENO,
602            TermTarget::Stderr => libc::STDERR_FILENO,
603            #[cfg(unix)]
604            TermTarget::ReadWritePair(ReadWritePair { ref write, .. }) => {
605                write.lock().unwrap().as_raw_fd()
606            }
607        }
608    }
609}
610
611#[cfg(windows)]
612impl AsRawHandle for Term {
613    fn as_raw_handle(&self) -> RawHandle {
614        use windows_sys::Win32::System::Console::{
615            GetStdHandle, STD_ERROR_HANDLE, STD_OUTPUT_HANDLE,
616        };
617
618        unsafe {
619            GetStdHandle(match self.inner.target {
620                TermTarget::Stdout => STD_OUTPUT_HANDLE,
621                TermTarget::Stderr => STD_ERROR_HANDLE,
622            }) as RawHandle
623        }
624    }
625}
626
627impl Write for Term {
628    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
629        match self.inner.buffer {
630            Some(ref buffer) => buffer.lock().unwrap().write_all(buf),
631            None => self.write_through(buf),
632        }?;
633        Ok(buf.len())
634    }
635
636    fn flush(&mut self) -> io::Result<()> {
637        Term::flush(self)
638    }
639}
640
641impl Write for &Term {
642    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
643        match self.inner.buffer {
644            Some(ref buffer) => buffer.lock().unwrap().write_all(buf),
645            None => self.write_through(buf),
646        }?;
647        Ok(buf.len())
648    }
649
650    fn flush(&mut self) -> io::Result<()> {
651        Term::flush(self)
652    }
653}
654
655impl Read for Term {
656    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
657        io::stdin().read(buf)
658    }
659}
660
661impl Read for &Term {
662    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
663        io::stdin().read(buf)
664    }
665}
666
667#[cfg(all(unix, not(target_arch = "wasm32")))]
668pub(crate) use crate::unix_term::*;
669#[cfg(target_arch = "wasm32")]
670pub(crate) use crate::wasm_term::*;
671#[cfg(windows)]
672pub(crate) use crate::windows_term::*;