use std::io::{self, Write};
use anyhow::Result;
use crossterm::{
QueueableCommand, execute,
terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate},
};
struct SynchronizedOutput<'a, W: Write> {
writer: Option<&'a mut W>,
}
impl<W: Write> SynchronizedOutput<'_, W> {
fn finish(&mut self) -> io::Result<()> {
if let Some(writer) = self.writer.take() {
execute!(writer, EndSynchronizedUpdate)?;
}
Ok(())
}
}
impl<W: Write> Drop for SynchronizedOutput<'_, W> {
fn drop(&mut self) {
let _ = self.finish();
}
}
pub(in crate::tui) fn draw_synchronized<W: Write, T>(
writer: &mut W,
draw: impl FnOnce(&mut W) -> Result<T>,
) -> Result<T> {
let mut output = SynchronizedOutput {
writer: Some(writer),
};
let writer = output.writer.as_deref_mut().expect("active frame writer");
writer.queue(BeginSynchronizedUpdate)?;
let draw_result = draw(writer);
let finish_result = output.finish();
let value = draw_result?;
finish_result?;
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::cursor::{MoveTo, Show};
#[derive(Default)]
struct RecordedOutput {
bytes: Vec<u8>,
flushes: Vec<Vec<u8>>,
fail_flush: bool,
}
impl Write for RecordedOutput {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.bytes.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
self.flushes.push(self.bytes.clone());
if self.fail_flush {
return Err(io::Error::other("flush failed"));
}
Ok(())
}
}
#[test]
fn synchronized_frame_wraps_scattered_writes_and_final_cursor_before_flush() {
let mut output = RecordedOutput::default();
let result = draw_synchronized(&mut output, |writer| {
execute!(writer, MoveTo(8, 2))?;
writer.write_all(b"animated cell")?;
execute!(writer, MoveTo(1, 4), Show)?;
Ok(42)
})
.unwrap();
assert_eq!(result, 42);
assert_eq!(
output.bytes,
b"\x1b[?2026h\x1b[3;9Hanimated cell\x1b[5;2H\x1b[?25h\x1b[?2026l"
);
assert_eq!(output.flushes.last(), Some(&output.bytes));
for partial in &output.flushes[..output.flushes.len() - 1] {
assert!(partial.starts_with(b"\x1b[?2026h"));
assert!(!partial.ends_with(b"\x1b[?2026l"));
}
}
#[test]
fn synchronized_frame_ends_and_flushes_on_draw_error() {
let mut output = RecordedOutput::default();
let result: Result<()> = draw_synchronized(&mut output, |writer| {
writer.write_all(b"partial frame")?;
anyhow::bail!("draw failed");
});
assert_eq!(result.unwrap_err().to_string(), "draw failed");
assert_eq!(output.bytes, b"\x1b[?2026hpartial frame\x1b[?2026l");
assert_eq!(output.flushes, vec![output.bytes.clone()]);
}
#[test]
fn synchronized_frame_ends_during_panic_unwind() {
let mut output = RecordedOutput::default();
let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _: Result<()> = draw_synchronized(&mut output, |writer| {
writer.write_all(b"partial frame")?;
panic!("draw panicked");
});
}));
assert!(panic.is_err());
assert_eq!(output.bytes, b"\x1b[?2026hpartial frame\x1b[?2026l");
assert_eq!(output.flushes, vec![output.bytes.clone()]);
}
#[test]
fn synchronized_frame_reports_flush_failure_without_hiding_draw_failure() {
for draw_fails in [false, true] {
let mut output = RecordedOutput {
fail_flush: true,
..RecordedOutput::default()
};
let result = draw_synchronized(&mut output, |_| {
if draw_fails {
anyhow::bail!("draw failed");
}
Ok(())
});
assert_eq!(
result.unwrap_err().to_string(),
if draw_fails {
"draw failed"
} else {
"flush failed"
}
);
assert!(output.bytes.ends_with(b"\x1b[?2026l"));
}
}
}