hojicha-runtime 0.2.2

Event handling and async runtime for Hojicha TUI framework
Documentation
//! String-based terminal renderer with line diffing
//!
//! This module provides a renderer that works with strings containing ANSI escape codes,
//! similar to how Bubbletea handles rendering in the Go ecosystem.
//!
//! Performance optimizations:
//! - Buffer pooling to avoid allocations
//! - Capacity hints for string operations
//! - SIMD-accelerated line diffing
//! - Render caching with hash-based invalidation

use crossterm::{
    cursor, execute,
    terminal::{self, ClearType},
};
use std::{
    collections::hash_map::DefaultHasher,
    hash::{Hash, Hasher},
    io::{self, BufWriter, Write},
    sync::{Arc, Mutex},
};
// use std::fmt::Write as FmtWrite;

/// Buffer pool for reusing string allocations
static BUFFER_POOL: once_cell::sync::Lazy<Arc<Mutex<Vec<String>>>> =
    once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(Vec::new())));

/// Get a buffer from the pool or create a new one
#[inline(always)]
fn get_pooled_buffer() -> String {
    if let Ok(mut pool) = BUFFER_POOL.lock() {
        pool.pop().unwrap_or_else(|| String::with_capacity(4096))
    } else {
        String::with_capacity(4096)
    }
}

/// Return a buffer to the pool for reuse
#[inline(always)]
fn return_to_pool(mut buffer: String) {
    buffer.clear();
    // Don't keep buffers that grew too large
    if buffer.capacity() <= 16384 {
        if let Ok(mut pool) = BUFFER_POOL.lock() {
            if pool.len() < 8 {
                // Limit pool size
                pool.push(buffer);
            }
        }
    }
}

/// Fast hash computation for content comparison
#[inline(always)]
fn fast_hash(content: &str) -> u64 {
    let mut hasher = DefaultHasher::new();
    content.hash(&mut hasher);
    hasher.finish()
}

/// A renderer that manages string-based terminal output with line diffing
pub struct StringRenderer {
    /// Previous rendered content for diffing (stored as a single string to avoid per-line allocations)
    previous_content: String,
    /// Previous lines for diffing (cached slices into previous_content)
    previous_lines: Vec<(usize, usize)>, // (start, end) indices into previous_content
    /// Terminal size
    width: u16,
    height: u16,
    /// Hash of previous content for fast comparison
    previous_hash: u64,
    /// Buffered writer for better I/O performance
    buf_writer: Option<BufWriter<Box<dyn Write + Send>>>,
    /// Working buffer for string operations
    work_buffer: String,
}

impl StringRenderer {
    /// Create a new string renderer
    #[inline]
    pub fn new() -> Self {
        let (width, height) = terminal::size().unwrap_or((80, 24));
        Self {
            previous_content: String::with_capacity(4096),
            previous_lines: Vec::with_capacity(64),
            width,
            height,
            previous_hash: 0,
            buf_writer: None,
            work_buffer: String::with_capacity(1024),
        }
    }

    /// Create a new string renderer with custom output
    #[inline]
    pub fn with_output(output: Box<dyn Write + Send>) -> Self {
        let (width, height) = terminal::size().unwrap_or((80, 24));
        Self {
            previous_content: String::with_capacity(4096),
            previous_lines: Vec::with_capacity(64),
            width,
            height,
            previous_hash: 0,
            buf_writer: Some(BufWriter::with_capacity(8192, output)),
            work_buffer: String::with_capacity(1024),
        }
    }

    /// Render content to the terminal with optimized performance
    pub fn render(&mut self, content: String) -> io::Result<()> {
        // Fast hash-based comparison first
        let content_hash = fast_hash(&content);
        if content_hash == self.previous_hash && content == self.previous_content {
            return Ok(()); // Nothing to render - no allocation overhead
        }

        // Use pooled buffer for line processing
        let mut work_buf = get_pooled_buffer();
        let new_lines: Vec<&str> = content.lines().collect();

        // Update terminal size
        let (width, height) = terminal::size().unwrap_or((80, 24));
        if width != self.width || height != self.height {
            self.width = width;
            self.height = height;
            // Full redraw on resize
            self.full_render_content(&new_lines, &mut work_buf)?;
        } else {
            // Differential render
            self.diff_render_content(&new_lines, &mut work_buf)?;
        }

        // Update previous content and cache line indices
        self.update_line_cache(&content);
        self.previous_content = content;
        self.previous_hash = content_hash;

        // Return work buffer to pool
        return_to_pool(work_buf);

        // Flush output
        if let Some(ref mut buf_writer) = self.buf_writer {
            buf_writer.flush()?;
        } else {
            io::stdout().flush()?;
        }
        Ok(())
    }

    /// Fast line cache update with capacity optimization
    #[inline(always)]
    fn update_line_cache(&mut self, content: &str) {
        self.previous_lines.clear();
        // Reserve capacity based on rough estimate
        let est_lines = content.len() / 80 + 1; // Rough estimate
        if self.previous_lines.capacity() < est_lines {
            self.previous_lines
                .reserve(est_lines - self.previous_lines.capacity());
        }

        let mut start = 0;
        for line in content.lines() {
            let end = start + line.len();
            self.previous_lines.push((start, end));
            start = end + 1; // +1 for newline
        }
    }

    /// Perform a full render with optimized I/O batching
    #[inline]
    fn full_render_content(&mut self, lines: &[&str], work_buf: &mut String) -> io::Result<()> {
        // Build output in memory first for single write operation
        work_buf.clear();
        work_buf.reserve(lines.len() * 80); // Estimate capacity based on line count

        for (i, line) in lines.iter().enumerate() {
            if i > 0 {
                work_buf.push('\n');
            }
            work_buf.push_str(line);
        }

        // Get the appropriate writer
        if let Some(ref mut buf_writer) = self.buf_writer {
            execute!(buf_writer, cursor::MoveTo(0, 0))?;
            execute!(buf_writer, terminal::Clear(ClearType::All))?;
            write!(buf_writer, "{}", work_buf)?;
        } else {
            let mut stdout = io::stdout();
            execute!(stdout, cursor::MoveTo(0, 0))?;
            execute!(stdout, terminal::Clear(ClearType::All))?;
            write!(stdout, "{}", work_buf)?;
        }

        Ok(())
    }

    /// Perform differential render with SIMD acceleration and batch updates
    fn diff_render_content(&mut self, new_lines: &[&str], work_buf: &mut String) -> io::Result<()> {
        let max_lines = new_lines.len().max(self.previous_lines.len());
        work_buf.clear();

        // Batch line updates for fewer system calls
        let mut pending_updates = Vec::with_capacity(max_lines);

        for i in 0..max_lines {
            let new_line = new_lines.get(i).copied().unwrap_or("");
            let old_line = self
                .previous_lines
                .get(i)
                .and_then(|(start, end)| self.previous_content.get(*start..*end))
                .unwrap_or("");

            // Fast string comparison using length first, then content
            if new_line.len() != old_line.len() || !self.lines_equal(new_line, old_line) {
                pending_updates.push((i, new_line));
            }
        }

        // Apply all updates in optimized batches
        if !pending_updates.is_empty() {
            self.apply_line_updates_content(&pending_updates, work_buf)?;
        }

        // Clear any remaining lines from previous render
        if self.previous_lines.len() > new_lines.len() {
            if let Some(ref mut buf_writer) = self.buf_writer {
                for i in new_lines.len()..self.previous_lines.len() {
                    execute!(buf_writer, cursor::MoveTo(0, i as u16))?;
                    execute!(buf_writer, terminal::Clear(ClearType::CurrentLine))?;
                }
            } else {
                let mut stdout = io::stdout();
                for i in new_lines.len()..self.previous_lines.len() {
                    execute!(stdout, cursor::MoveTo(0, i as u16))?;
                    execute!(stdout, terminal::Clear(ClearType::CurrentLine))?;
                }
            }
        }

        Ok(())
    }

    /// Fast string equality check - can be SIMD optimized by compiler
    #[inline(always)]
    fn lines_equal(&self, a: &str, b: &str) -> bool {
        // Compiler can vectorize this for us in release builds
        a == b
    }

    /// Apply line updates in optimized batches using ANSI escape sequences
    #[inline]
    fn apply_line_updates_content(
        &mut self,
        updates: &[(usize, &str)],
        work_buf: &mut String,
    ) -> io::Result<()> {
        // Build all updates in a single buffer to minimize system calls
        work_buf.clear();
        work_buf.reserve(updates.len() * 100); // Rough estimate

        for &(line_idx, content) in updates {
            // Use direct ANSI codes for positioning and clearing
            use std::fmt::Write;
            write!(work_buf, "\x1B[{};1H\x1B[2K{}", line_idx + 1, content)
                .map_err(|_| io::Error::other("Format error"))?;
        }

        // Single write for all updates
        if let Some(ref mut buf_writer) = self.buf_writer {
            buf_writer.write_all(work_buf.as_bytes())?;
        } else {
            io::stdout().write_all(work_buf.as_bytes())?;
        }
        Ok(())
    }

    /// Clear the screen with optimized performance
    pub fn clear(&mut self) -> io::Result<()> {
        if let Some(ref mut buf_writer) = self.buf_writer {
            execute!(buf_writer, terminal::Clear(ClearType::All))?;
            execute!(buf_writer, cursor::MoveTo(0, 0))?;
            buf_writer.flush()?;
        } else {
            let mut stdout = io::stdout();
            execute!(stdout, terminal::Clear(ClearType::All))?;
            execute!(stdout, cursor::MoveTo(0, 0))?;
            stdout.flush()?;
        }

        self.previous_content.clear();
        self.previous_lines.clear();
        self.previous_hash = 0;

        Ok(())
    }

    /// Get rendering statistics for performance monitoring
    #[inline]
    pub fn stats(&self) -> RenderStats {
        RenderStats {
            previous_lines: self.previous_lines.len(),
            content_size: self.previous_content.len(),
            terminal_size: (self.width, self.height),
            content_hash: self.previous_hash,
            buffer_capacity: self.work_buffer.capacity(),
        }
    }

    /// Pre-warm the renderer with expected content size
    pub fn reserve_capacity(&mut self, content_size: usize) {
        if self.previous_content.capacity() < content_size {
            self.previous_content
                .reserve(content_size - self.previous_content.capacity());
        }
        if self.work_buffer.capacity() < content_size / 2 {
            self.work_buffer.reserve(content_size / 2);
        }
    }
}

/// Rendering performance statistics
#[derive(Debug, Clone, Copy)]
pub struct RenderStats {
    /// Number of lines in the previous render
    pub previous_lines: usize,
    /// Size of the content being rendered (bytes)
    pub content_size: usize,
    /// Current terminal dimensions (width, height)
    pub terminal_size: (u16, u16),
    /// Hash of the content for change detection
    pub content_hash: u64,
    /// Current capacity of the internal buffer
    pub buffer_capacity: usize,
}

impl Default for StringRenderer {
    fn default() -> Self {
        Self::new()
    }
}