aboutsummaryrefslogtreecommitdiffstats
path: root/src/output.rs
blob: 4b2fb0667e2fe54a44cd295f3139e2514e53bd46 (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
use crate::error::*;

use futures_lite::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).
    pub async fn new() -> Result<Self> {
        write_stdout(
            &mut blocking::Unblock::new(std::io::stdout()),
            crate::INIT,
        )
        .await?;
        Ok(Self { cleaned_up: false })
    }

    /// Switch back from alternate screen mode early.
    pub async fn cleanup(&mut self) -> Result<()> {
        if self.cleaned_up {
            return Ok(());
        }
        self.cleaned_up = true;
        write_stdout(
            &mut blocking::Unblock::new(std::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) {
        futures_lite::future::block_on(async {
            let _ = self.cleanup().await;
        });
    }
}

/// 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: blocking::Unblock<std::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.
    pub async fn new() -> 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.
    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: blocking::Unblock::new(std::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.
    pub async fn refresh(&mut self) -> Result<()> {
        let diff = self.next().screen().state_diff(self.cur().screen());
        write_stdout(&mut self.stdout, &diff).await?;
        self.cur_mut().process(&diff);
        Ok(())
    }
}

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