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).
///
/// # Errors
/// * `Error::WriteStdout`: failed to write initialization to stdout
pub async fn new() -> crate::error::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.
///
/// # 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 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 {
// https://github.com/rust-lang/rust-clippy/issues/8003
#[allow(clippy::let_underscore_drop)]
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.
///
/// # 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: 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.
///
/// # 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 blocking::Unblock<std::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(())
}