Skip to main content

console/
term.rs

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