textmode/blocking/
output.rs

1use std::io::Write as _;
2
3use crate::private::Output as _;
4
5/// Switches the terminal on `stdout` to alternate screen mode, and restores
6/// it when this object goes out of scope.
7pub struct ScreenGuard {
8    cleaned_up: bool,
9}
10
11impl ScreenGuard {
12    /// Switches the terminal on `stdout` to alternate screen mode and returns
13    /// a guard object. This is typically called as part of
14    /// [`Output::new`](Output::new).
15    ///
16    /// # Errors
17    /// * `Error::WriteStdout`: failed to write initialization to stdout
18    pub fn new() -> crate::error::Result<Self> {
19        write_stdout(crate::INIT)?;
20        Ok(Self { cleaned_up: false })
21    }
22
23    /// Switch back from alternate screen mode early.
24    ///
25    /// # Errors
26    /// * `Error::WriteStdout`: failed to write deinitialization to stdout
27    pub fn cleanup(&mut self) -> crate::error::Result<()> {
28        if self.cleaned_up {
29            return Ok(());
30        }
31        self.cleaned_up = true;
32        write_stdout(crate::DEINIT)
33    }
34}
35
36impl Drop for ScreenGuard {
37    /// Calls `cleanup`.
38    fn drop(&mut self) {
39        let _ = self.cleanup();
40    }
41}
42
43/// Manages drawing to the terminal on `stdout`.
44///
45/// Most functionality is provided by the [`Textmode`](crate::Textmode) trait.
46/// You should call those trait methods to draw to the in-memory screen, and
47/// then call [`refresh`](Output::refresh) when you want to update the
48/// terminal on `stdout`.
49pub struct Output {
50    screen: Option<ScreenGuard>,
51
52    cur: vt100::Parser,
53    next: vt100::Parser,
54}
55
56impl crate::private::Output for Output {
57    fn cur(&self) -> &vt100::Parser {
58        &self.cur
59    }
60
61    fn cur_mut(&mut self) -> &mut vt100::Parser {
62        &mut self.cur
63    }
64
65    fn next(&self) -> &vt100::Parser {
66        &self.next
67    }
68
69    fn next_mut(&mut self) -> &mut vt100::Parser {
70        &mut self.next
71    }
72}
73
74impl crate::Textmode for Output {}
75
76impl Output {
77    /// Creates a new `Output` instance containing a
78    /// [`ScreenGuard`](ScreenGuard) instance.
79    ///
80    /// # Errors
81    /// * `Error::WriteStdout`: failed to write initialization to stdout
82    pub fn new() -> crate::error::Result<Self> {
83        let mut self_ = Self::new_without_screen();
84        self_.screen = Some(ScreenGuard::new()?);
85        Ok(self_)
86    }
87
88    /// Creates a new `Output` instance without creating a
89    /// [`ScreenGuard`](ScreenGuard) instance.
90    #[must_use]
91    pub fn new_without_screen() -> Self {
92        let (rows, cols) = match terminal_size::terminal_size() {
93            Some((terminal_size::Width(w), terminal_size::Height(h))) => {
94                (h, w)
95            }
96            _ => (24, 80),
97        };
98        let cur = vt100::Parser::new(rows, cols, 0);
99        let next = vt100::Parser::new(rows, cols, 0);
100
101        Self {
102            screen: None,
103            cur,
104            next,
105        }
106    }
107
108    /// Removes the [`ScreenGuard`](ScreenGuard) instance stored in this
109    /// `Output` instance and returns it. This can be useful if you need to
110    /// manage the lifetime of the [`ScreenGuard`](ScreenGuard) instance
111    /// separately.
112    pub fn take_screen_guard(&mut self) -> Option<ScreenGuard> {
113        self.screen.take()
114    }
115
116    /// Draws the in-memory screen to the terminal on `stdout`. This is done
117    /// using a diff mechanism to only update the parts of the terminal which
118    /// are different from the in-memory screen.
119    ///
120    /// # Errors
121    /// * `Error::WriteStdout`: failed to write screen state to stdout
122    pub fn refresh(&mut self) -> crate::error::Result<()> {
123        let diff = self.next().screen().state_diff(self.cur().screen());
124        write_stdout(&diff)?;
125        self.cur_mut().process(&diff);
126        Ok(())
127    }
128
129    /// Draws the in-memory screen to the terminal on `stdout`. This clears
130    /// the screen and redraws it from scratch, rather than using a diff
131    /// mechanism like `refresh`. This can be useful when the current state of
132    /// the terminal screen is unknown, such as after the terminal has been
133    /// resized.
134    ///
135    /// # Errors
136    /// * `Error::WriteStdout`: failed to write screen state to stdout
137    pub fn hard_refresh(&mut self) -> crate::error::Result<()> {
138        let contents = self.next().screen().state_formatted();
139        write_stdout(&contents)?;
140        self.cur_mut().process(&contents);
141        Ok(())
142    }
143}
144
145fn write_stdout(buf: &[u8]) -> crate::error::Result<()> {
146    let mut stdout = std::io::stdout();
147    stdout
148        .write_all(buf)
149        .map_err(crate::error::Error::WriteStdout)?;
150    stdout.flush().map_err(crate::error::Error::WriteStdout)?;
151    Ok(())
152}