aboutsummaryrefslogtreecommitdiffstats
path: root/src/output.rs
blob: 483de6b5fc9a2de108b4638bc4700b06d7a7112e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
use tokio::io::AsyncWriteExt as _;

use crate::private::Output as _;

/// Switches the terminal on `stdout` to alternate screen mode, and restores
/// it when this object goes out of scope.
pub struct ScreenGuard {
    cleaned_up: bool,
}

impl ScreenGuard {
    /// Switches the terminal on `stdout` to alternate screen mode and returns
    /// a guard object. This is typically called as part of
    /// [`Output::new`](Output::new).
    ///
    /// # Errors
    /// * `Error::WriteStdout`: failed to write initialization to stdout
    pub async fn new() -> crate::error::Result<Self> {
        write_stdout(&mut tokio::io::stdout(), crate::INIT).await?;
        Ok(Self { cleaned_up: false })
    }

    /// Switch back from alternate screen mode early.
    ///
    /// # Errors
    /// * `Error::WriteStdout`: failed to write deinitialization to stdout
    pub async fn cleanup(&mut self) -> crate::error::Result<()> {
        if self.cleaned_up {
            return Ok(());
        }
        self.cleaned_up = true;
        write_stdout(&mut tokio::io::stdout(), crate::DEINIT).await
    }
}

impl Drop for ScreenGuard {
    /// Calls `cleanup`. Note that this may block, due to Rust's current lack
    /// of an async drop mechanism. If this could be a problem, you should
    /// call `cleanup` manually instead.
    fn drop(&mut self) {
        // doesn't literally call `cleanup`, because calling spawn_blocking
        // while the tokio runtime is in the process of shutting down doesn't
        // work (spawn_blocking tasks are cancelled if the runtime starts
        // shutting down before the task body starts running), and using
        // block_in_place/block_on doesn't work on the current_thread runtime,
        // but should be kept in sync with the actual things that `cleanup`
        // does.
        use std::io::Write as _;

        if !self.cleaned_up {
            let mut stdout = std::io::stdout();
            #[allow(clippy::let_underscore_drop)]
            let _ = stdout.write_all(crate::DEINIT);
            #[allow(clippy::let_underscore_drop)]
            let _ = stdout.flush();
        }
    }
}

/// Manages drawing to the terminal on `stdout`.
///
/// Most functionality is provided by the [`Textmode`](crate::Textmode) trait.
/// You should call those trait methods to draw to the in-memory screen, and
/// then call [`refresh`](Output::refresh) when you want to update the
/// terminal on `stdout`.
pub struct Output {
    stdout: tokio::io::Stdout,
    screen: Option<ScreenGuard>,

    cur: vt100::Parser,
    next: vt100::Parser,
}

impl crate::private::Output for Output {
    fn cur(&self) -> &vt100::Parser {
        &self.cur
    }

    fn cur_mut(&mut self) -> &mut vt100::Parser {
        &mut self.cur
    }

    fn next(&self) -> &vt100::Parser {
        &self.next
    }

    fn next_mut(&mut self) -> &mut vt100::Parser {
        &mut self.next
    }
}

impl crate::Textmode for Output {}

impl Output {
    /// Creates a new `Output` instance containing a
    /// [`ScreenGuard`](ScreenGuard) instance.
    ///
    /// # Errors
    /// * `Error::WriteStdout`: failed to write initialization to stdout
    pub async fn new() -> crate::error::Result<Self> {
        let mut self_ = Self::new_without_screen();
        self_.screen = Some(ScreenGuard::new().await?);
        Ok(self_)
    }

    /// Creates a new `Output` instance without creating a
    /// [`ScreenGuard`](ScreenGuard) instance.
    #[must_use]
    pub fn new_without_screen() -> Self {
        let (rows, cols) = match terminal_size::terminal_size() {
            Some((terminal_size::Width(w), terminal_size::Height(h))) => {
                (h, w)
            }
            _ => (24, 80),
        };
        let cur = vt100::Parser::new(rows, cols, 0);
        let next = vt100::Parser::new(rows, cols, 0);

        Self {
            stdout: tokio::io::stdout(),
            screen: None,
            cur,
            next,
        }
    }

    /// Removes the [`ScreenGuard`](ScreenGuard) instance stored in this
    /// `Output` instance and returns it. This can be useful if you need to
    /// manage the lifetime of the [`ScreenGuard`](ScreenGuard) instance
    /// separately.
    pub fn take_screen_guard(&mut self) -> Option<ScreenGuard> {
        self.screen.take()
    }

    /// Draws the in-memory screen to the terminal on `stdout`. This is done
    /// using a diff mechanism to only update the parts of the terminal which
    /// are different from the in-memory screen.
    ///
    /// # Errors
    /// * `Error::WriteStdout`: failed to write screen state to stdout
    pub async fn refresh(&mut self) -> crate::error::Result<()> {
        let diff = self.next().screen().state_diff(self.cur().screen());
        write_stdout(&mut self.stdout, &diff).await?;
        self.cur_mut().process(&diff);
        Ok(())
    }

    /// Draws the in-memory screen to the terminal on `stdout`. This clears
    /// the screen and redraws it from scratch, rather than using a diff
    /// mechanism like `refresh`. This can be useful when the current state of
    /// the terminal screen is unknown, such as after the terminal has been
    /// resized.
    ///
    /// # Errors
    /// * `Error::WriteStdout`: failed to write screen state to stdout
    pub async fn hard_refresh(&mut self) -> crate::error::Result<()> {
        let contents = self.next().screen().state_formatted();
        write_stdout(&mut self.stdout, &contents).await?;
        self.cur_mut().process(&contents);
        Ok(())
    }
}

async fn write_stdout(
    stdout: &mut tokio::io::Stdout,
    buf: &[u8],
) -> crate::error::Result<()> {
    stdout
        .write_all(buf)
        .await
        .map_err(crate::error::Error::WriteStdout)?;
    stdout
        .flush()
        .await
        .map_err(crate::error::Error::WriteStdout)?;
    Ok(())
}