console/
term.rs

1use std::fmt::{Debug, Display};
2use std::io::{self, Read, Write};
3use std::sync::{Arc, Mutex, RwLock};
4
5#[cfg(unix)]
6use std::os::unix::io::{AsRawFd, RawFd};
7#[cfg(windows)]
8use std::os::windows::io::{AsRawHandle, RawHandle};
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)]
41pub struct 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<'a> TermFeatures<'a> {
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 this terminal is an msys terminal.
82    ///
83    /// This is sometimes useful to disable features that are known to not
84    /// work on msys terminals or require special handling.
85    #[inline]
86    pub fn is_msys_tty(&self) -> bool {
87        #[cfg(windows)]
88        {
89            msys_tty_on(self.0)
90        }
91        #[cfg(not(windows))]
92        {
93            false
94        }
95    }
96
97    /// Check if this terminal wants emojis.
98    #[inline]
99    pub fn wants_emoji(&self) -> bool {
100        self.is_attended() && wants_emoji()
101    }
102
103    /// Return the family of the terminal.
104    #[inline]
105    pub fn family(&self) -> TermFamily {
106        if !self.is_attended() {
107            return TermFamily::File;
108        }
109        #[cfg(windows)]
110        {
111            TermFamily::WindowsConsole
112        }
113        #[cfg(all(unix, not(target_arch = "wasm32")))]
114        {
115            TermFamily::UnixTerm
116        }
117        #[cfg(target_arch = "wasm32")]
118        {
119            TermFamily::Dummy
120        }
121    }
122}
123
124/// Abstraction around a terminal.
125///
126/// A terminal can be cloned.  If a buffer is used it's shared across all
127/// clones which means it largely acts as a handle.
128#[derive(Clone, Debug)]
129pub struct Term {
130    inner: Arc<TermInner>,
131    pub(crate) is_msys_tty: bool,
132    pub(crate) is_tty: bool,
133}
134
135impl Term {
136    fn with_inner(inner: TermInner) -> Term {
137        let mut term = Term {
138            inner: Arc::new(inner),
139            is_msys_tty: false,
140            is_tty: false,
141        };
142
143        term.is_msys_tty = term.features().is_msys_tty();
144        term.is_tty = term.features().is_attended();
145        term
146    }
147
148    /// Return a new unbuffered terminal.
149    #[inline]
150    pub fn stdout() -> Term {
151        Term::with_inner(TermInner {
152            target: TermTarget::Stdout,
153            buffer: None,
154            prompt: RwLock::new(String::new()),
155            prompt_guard: Mutex::new(()),
156        })
157    }
158
159    /// Return a new unbuffered terminal to stderr.
160    #[inline]
161    pub fn stderr() -> Term {
162        Term::with_inner(TermInner {
163            target: TermTarget::Stderr,
164            buffer: None,
165            prompt: RwLock::new(String::new()),
166            prompt_guard: Mutex::new(()),
167        })
168    }
169
170    /// Return a new buffered terminal.
171    pub fn buffered_stdout() -> Term {
172        Term::with_inner(TermInner {
173            target: TermTarget::Stdout,
174            buffer: Some(Mutex::new(vec![])),
175            prompt: RwLock::new(String::new()),
176            prompt_guard: Mutex::new(()),
177        })
178    }
179
180    /// Return a new buffered terminal to stderr.
181    pub fn buffered_stderr() -> Term {
182        Term::with_inner(TermInner {
183            target: TermTarget::Stderr,
184            buffer: Some(Mutex::new(vec![])),
185            prompt: RwLock::new(String::new()),
186            prompt_guard: Mutex::new(()),
187        })
188    }
189
190    /// Return a terminal for the given Read/Write pair styled like stderr.
191    #[cfg(unix)]
192    pub fn read_write_pair<R, W>(read: R, write: W) -> Term
193    where
194        R: Read + Debug + AsRawFd + Send + 'static,
195        W: Write + Debug + AsRawFd + Send + 'static,
196    {
197        Self::read_write_pair_with_style(read, write, Style::new().for_stderr())
198    }
199
200    /// Return a terminal for the given Read/Write pair.
201    #[cfg(unix)]
202    pub fn read_write_pair_with_style<R, W>(read: R, write: W, style: Style) -> Term
203    where
204        R: Read + Debug + AsRawFd + Send + 'static,
205        W: Write + Debug + AsRawFd + Send + 'static,
206    {
207        Term::with_inner(TermInner {
208            target: TermTarget::ReadWritePair(ReadWritePair {
209                read: Arc::new(Mutex::new(read)),
210                write: Arc::new(Mutex::new(write)),
211                style,
212            }),
213            buffer: None,
214            prompt: RwLock::new(String::new()),
215            prompt_guard: Mutex::new(()),
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 form 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() && chars.pop().is_some() {
340                            slf.clear_chars(1)?;
341                        }
342                        slf.flush()?;
343                    }
344                    Key::Char(chr) => {
345                        chars.push(chr);
346                        let mut bytes_char = [0; 4];
347                        chr.encode_utf8(&mut bytes_char);
348                        slf.write_str(chr.encode_utf8(&mut bytes_char))?;
349                        slf.flush()?;
350                    }
351                    Key::Enter => {
352                        slf.write_through(format!("\n{}", initial).as_bytes())?;
353                        break;
354                    }
355                    _ => (),
356                }
357            }
358            Ok(chars.iter().skip(prefix_len).collect::<String>())
359        }
360        let ret = read_line_internal(self, initial);
361
362        *self.inner.prompt.write().unwrap() = String::new();
363        ret
364    }
365
366    /// Read a line of input securely.
367    ///
368    /// This is similar to `read_line` but will not echo the output.  This
369    /// also switches the terminal into a different mode where not all
370    /// characters might be accepted.
371    pub fn read_secure_line(&self) -> io::Result<String> {
372        if !self.is_tty {
373            return Ok("".into());
374        }
375        match read_secure() {
376            Ok(rv) => {
377                self.write_line("")?;
378                Ok(rv)
379            }
380            Err(err) => Err(err),
381        }
382    }
383
384    /// Flush internal buffers.
385    ///
386    /// This forces the contents of the internal buffer to be written to
387    /// the terminal.  This is unnecessary for unbuffered terminals which
388    /// will automatically flush.
389    pub fn flush(&self) -> io::Result<()> {
390        if let Some(ref buffer) = self.inner.buffer {
391            let mut buffer = buffer.lock().unwrap();
392            if !buffer.is_empty() {
393                self.write_through(&buffer[..])?;
394                buffer.clear();
395            }
396        }
397        Ok(())
398    }
399
400    /// Check if the terminal is indeed a terminal.
401    #[inline]
402    pub fn is_term(&self) -> bool {
403        self.is_tty
404    }
405
406    /// Check for common terminal features.
407    #[inline]
408    pub fn features(&self) -> TermFeatures<'_> {
409        TermFeatures(self)
410    }
411
412    /// Return the terminal size in rows and columns or gets sensible defaults.
413    #[inline]
414    pub fn size(&self) -> (u16, u16) {
415        self.size_checked().unwrap_or((24, DEFAULT_WIDTH))
416    }
417
418    /// Return the terminal size in rows and columns.
419    ///
420    /// If the size cannot be reliably determined `None` is returned.
421    #[inline]
422    pub fn size_checked(&self) -> Option<(u16, u16)> {
423        terminal_size(self)
424    }
425
426    /// Move the cursor to row `x` and column `y`. Values are 0-based.
427    #[inline]
428    pub fn move_cursor_to(&self, x: usize, y: usize) -> io::Result<()> {
429        move_cursor_to(self, x, y)
430    }
431
432    /// Move the cursor up by `n` lines, if possible.
433    ///
434    /// If there are less than `n` lines above the current cursor position,
435    /// the cursor is moved to the top line of the terminal (i.e., as far up as possible).
436    #[inline]
437    pub fn move_cursor_up(&self, n: usize) -> io::Result<()> {
438        move_cursor_up(self, n)
439    }
440
441    /// Move the cursor down by `n` lines, if possible.
442    ///
443    /// If there are less than `n` lines below the current cursor position,
444    /// the cursor is moved to the bottom line of the terminal (i.e., as far down as possible).
445    #[inline]
446    pub fn move_cursor_down(&self, n: usize) -> io::Result<()> {
447        move_cursor_down(self, n)
448    }
449
450    /// Move the cursor `n` characters to the left, if possible.
451    ///
452    /// If there are fewer than `n` characters to the left of the current cursor position,
453    /// the cursor is moved to the beginning of the line (i.e., as far to the left as possible).
454    #[inline]
455    pub fn move_cursor_left(&self, n: usize) -> io::Result<()> {
456        move_cursor_left(self, n)
457    }
458
459    /// Move the cursor `n` characters to the right.
460    ///
461    /// If there are fewer than `n` characters to the right of the current cursor position,
462    /// the cursor is moved to the end of the current line (i.e., as far to the right as possible).
463    #[inline]
464    pub fn move_cursor_right(&self, n: usize) -> io::Result<()> {
465        move_cursor_right(self, n)
466    }
467
468    /// Clear the current line.
469    ///
470    /// Position the cursor at the beginning of the current line.
471    #[inline]
472    pub fn clear_line(&self) -> io::Result<()> {
473        clear_line(self)
474    }
475
476    /// Clear the last `n` lines before the current line.
477    ///
478    /// Position the cursor at the beginning of the first line that was cleared.
479    pub fn clear_last_lines(&self, n: usize) -> io::Result<()> {
480        self.move_cursor_up(n)?;
481        for _ in 0..n {
482            self.clear_line()?;
483            self.move_cursor_down(1)?;
484        }
485        self.move_cursor_up(n)?;
486        Ok(())
487    }
488
489    /// Clear the entire screen.
490    ///
491    /// Move the cursor to the upper left corner of the screen.
492    #[inline]
493    pub fn clear_screen(&self) -> io::Result<()> {
494        clear_screen(self)
495    }
496
497    /// Clear everything from the current cursor position to the end of the screen.
498    /// The cursor stays in its position.
499    #[inline]
500    pub fn clear_to_end_of_screen(&self) -> io::Result<()> {
501        clear_to_end_of_screen(self)
502    }
503
504    /// Clear the last `n` characters of the current line.
505    #[inline]
506    pub fn clear_chars(&self, n: usize) -> io::Result<()> {
507        clear_chars(self, n)
508    }
509
510    /// Set the terminal title.
511    pub fn set_title<T: Display>(&self, title: T) {
512        if !self.is_tty {
513            return;
514        }
515        set_title(title);
516    }
517
518    /// Make the cursor visible again.
519    #[inline]
520    pub fn show_cursor(&self) -> io::Result<()> {
521        show_cursor(self)
522    }
523
524    /// Hide the cursor.
525    #[inline]
526    pub fn hide_cursor(&self) -> io::Result<()> {
527        hide_cursor(self)
528    }
529
530    // helpers
531
532    #[cfg(all(windows, feature = "windows-console-colors"))]
533    fn write_through(&self, bytes: &[u8]) -> io::Result<()> {
534        if self.is_msys_tty || !self.is_tty {
535            self.write_through_common(bytes)
536        } else {
537            match self.inner.target {
538                TermTarget::Stdout => console_colors(self, Console::stdout()?, bytes),
539                TermTarget::Stderr => console_colors(self, Console::stderr()?, bytes),
540            }
541        }
542    }
543
544    #[cfg(not(all(windows, feature = "windows-console-colors")))]
545    fn write_through(&self, bytes: &[u8]) -> io::Result<()> {
546        self.write_through_common(bytes)
547    }
548
549    pub(crate) fn write_through_common(&self, bytes: &[u8]) -> io::Result<()> {
550        match self.inner.target {
551            TermTarget::Stdout => {
552                io::stdout().write_all(bytes)?;
553                io::stdout().flush()?;
554            }
555            TermTarget::Stderr => {
556                io::stderr().write_all(bytes)?;
557                io::stderr().flush()?;
558            }
559            #[cfg(unix)]
560            TermTarget::ReadWritePair(ReadWritePair { ref write, .. }) => {
561                let mut write = write.lock().unwrap();
562                write.write_all(bytes)?;
563                write.flush()?;
564            }
565        }
566        Ok(())
567    }
568}
569
570/// A fast way to check if the application has a user attended for stdout.
571///
572/// This means that stdout is connected to a terminal instead of a
573/// file or redirected by other means. This is a shortcut for
574/// checking the `is_attended` feature on the stdout terminal.
575#[inline]
576pub fn user_attended() -> bool {
577    Term::stdout().features().is_attended()
578}
579
580/// A fast way to check if the application has a user attended for stderr.
581///
582/// This means that stderr is connected to a terminal instead of a
583/// file or redirected by other means. This is a shortcut for
584/// checking the `is_attended` feature on the stderr terminal.
585#[inline]
586pub fn user_attended_stderr() -> bool {
587    Term::stderr().features().is_attended()
588}
589
590#[cfg(unix)]
591impl AsRawFd for Term {
592    fn as_raw_fd(&self) -> RawFd {
593        match self.inner.target {
594            TermTarget::Stdout => libc::STDOUT_FILENO,
595            TermTarget::Stderr => libc::STDERR_FILENO,
596            TermTarget::ReadWritePair(ReadWritePair { ref write, .. }) => {
597                write.lock().unwrap().as_raw_fd()
598            }
599        }
600    }
601}
602
603#[cfg(windows)]
604impl AsRawHandle for Term {
605    fn as_raw_handle(&self) -> RawHandle {
606        use windows_sys::Win32::System::Console::{
607            GetStdHandle, STD_ERROR_HANDLE, STD_OUTPUT_HANDLE,
608        };
609
610        unsafe {
611            GetStdHandle(match self.inner.target {
612                TermTarget::Stdout => STD_OUTPUT_HANDLE,
613                TermTarget::Stderr => STD_ERROR_HANDLE,
614            }) as RawHandle
615        }
616    }
617}
618
619impl Write for Term {
620    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
621        match self.inner.buffer {
622            Some(ref buffer) => buffer.lock().unwrap().write_all(buf),
623            None => self.write_through(buf),
624        }?;
625        Ok(buf.len())
626    }
627
628    fn flush(&mut self) -> io::Result<()> {
629        Term::flush(self)
630    }
631}
632
633impl<'a> Write for &'a Term {
634    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
635        match self.inner.buffer {
636            Some(ref buffer) => buffer.lock().unwrap().write_all(buf),
637            None => self.write_through(buf),
638        }?;
639        Ok(buf.len())
640    }
641
642    fn flush(&mut self) -> io::Result<()> {
643        Term::flush(self)
644    }
645}
646
647impl Read for Term {
648    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
649        io::stdin().read(buf)
650    }
651}
652
653impl<'a> Read for &'a Term {
654    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
655        io::stdin().read(buf)
656    }
657}
658
659#[cfg(all(unix, not(target_arch = "wasm32")))]
660pub use crate::unix_term::*;
661#[cfg(target_arch = "wasm32")]
662pub use crate::wasm_term::*;
663#[cfg(windows)]
664pub use crate::windows_term::*;