clankerdiff-ratatui 0.3.2

Embeddable Ratatui diff review widget
Documentation
use clankerdiff_markdown::MarkdownStream;
use clankerdiff_ratatui::{
    MarkdownCommitError, MarkdownLayoutOptions, MarkdownRenderer, MarkdownRow,
    StreamingMarkdownPolicy, StreamingMarkdownState,
};
use clankerdiff_syntax::SyntaxHighlighter;
use clankerdiff_theme::{ReviewTheme, Rgba};
use std::{error::Error, sync::Arc};

const VIEWPORT_ROWS: usize = 4;

struct NativeHistory {
    rows: Vec<String>,
    fail_next_write: bool,
}

impl NativeHistory {
    fn write(&mut self, rows: &[Arc<MarkdownRow>]) -> Result<(), String> {
        if std::mem::take(&mut self.fail_next_write) {
            return Err("terminal write failed".to_owned());
        }
        self.rows
            .extend(rows.iter().map(|row| row.line.to_string()));
        Ok(())
    }
}

fn main() -> Result<(), Box<dyn Error>> {
    let renderer = MarkdownRenderer::new();
    let options = MarkdownLayoutOptions {
        width: 40,
        ..MarkdownLayoutOptions::default()
    };
    let mut theme = ReviewTheme::default();
    let mut highlighter = SyntaxHighlighter::default();
    let mut stream = MarkdownStream::new();
    let mut state = StreamingMarkdownState::new(StreamingMarkdownPolicy::Terminal);
    let mut history = NativeHistory {
        rows: Vec::new(),
        fail_next_write: false,
    };
    let mut live: Vec<Arc<MarkdownRow>> = Vec::new();
    let mut revision = 0;

    let chunks = [
        "Streaming contract\n",
        "===\n\n",
        "The heading above was a plain paragraph until its underline arrived.\n\n",
        "```rust\nfn main() {\n",
        "    println!(\"hello\");\n",
        "}\n```\n\n",
        "See [the docs] for details.\n\n",
        "[the docs]: https://example.com\n",
    ];
    for (index, chunk) in chunks.iter().enumerate() {
        stream.push(chunk);
        renderer.render_stream_layout(&mut state, &stream, options, &theme, &mut highlighter)?;
        let update = state.update_since(revision);
        live.truncate(update.first_changed_row - state.committed_rows());
        live.extend(update.replacement.iter().cloned());
        revision = update.revision;

        history.fail_next_write = index == 3;
        let overflow = live.len().saturating_sub(VIEWPORT_ROWS);
        let pending = overflow.min(live.len().saturating_sub(1));
        if pending > 0 {
            match history.write(&live[..pending]) {
                Ok(()) => {
                    let end = state.committed_rows() + pending;
                    state.commit_rows(revision, end)?;
                    live.drain(..pending);
                }
                Err(error) => println!("kept {pending} rows live: {error}"),
            }
        }
        println!(
            "revision {revision}: {} committed, {} live",
            state.committed_rows(),
            live.len()
        );
    }

    stream.finish();
    theme.markdown.heading = Rgba::new(200, 120, 40, 255);
    renderer.render_stream_layout(&mut state, &stream, options, &theme, &mut highlighter)?;
    let update = state.update_since(revision);
    assert!(update.first_changed_row >= state.committed_rows());
    live.truncate(update.first_changed_row - state.committed_rows());
    live.extend(update.replacement.iter().cloned());

    match state.commit_rows(update.revision.saturating_sub(1), 1) {
        Err(MarkdownCommitError::StaleRevision { .. }) => println!("stale commit rejected"),
        other => println!("unexpected commit result: {other:?}"),
    }

    println!("--- native history ---");
    for row in &history.rows {
        println!("{row}");
    }
    println!("--- live region ---");
    for row in &live {
        println!("{}", row.line);
    }
    println!("{:?}", state.take_stats());
    Ok(())
}