magi-code 0.80.1

Repository-aware CLI coding agent for terminal work
Documentation
//! Present all cell, cursor, and image writes as one terminal update.

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();
    }
}

/// Keep intermediate cursor positions off screen, including image writes.
/// The caller holds stdout's lock; TestBackend draws need no terminal commands.
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);
    // Always end and flush, even when drawing failed. Preserve the draw error.
    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"));
        }
    }
}