boxen 0.4.0

A Rust library for creating styled terminal boxes around text with performance optimizations
Documentation
/// Unicode width caching for improved performance
///
/// This module provides an LRU cache for Unicode width calculations to avoid
/// redundant expensive operations. The cache is thread-local for lock-free access.
#[cfg(feature = "width-cache")]
use ahash::AHasher;
#[cfg(feature = "width-cache")]
use lru::LruCache;
#[cfg(feature = "width-cache")]
use std::cell::RefCell;
#[cfg(feature = "width-cache")]
use std::hash::{Hash, Hasher};
#[cfg(feature = "width-cache")]
use std::num::NonZeroUsize;
use unicode_width::UnicodeWidthStr;

/// Default cache size (number of entries)
#[cfg(feature = "width-cache")]
const DEFAULT_CACHE_SIZE: usize = 1024;

/// Cache statistics for monitoring performance
#[cfg(feature = "width-cache")]
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
    /// Number of cache hits
    pub hits: usize,
    /// Number of cache misses
    pub misses: usize,
    /// Number of cache evictions
    pub evictions: usize,
}

#[cfg(feature = "width-cache")]
impl CacheStats {
    /// Calculate cache hit rate as a percentage
    #[must_use]
    pub fn hit_rate(&self) -> f64 {
        let total = self.hits + self.misses;
        if total == 0 {
            0.0
        } else {
            // Allow precision loss: Cache hit rates are informational statistics
            // where slight precision loss at extreme values (>2^52 cache operations)
            // is acceptable. In practice, cache sizes are much smaller and precision
            // loss will never occur in real-world usage.
            #[allow(clippy::cast_precision_loss)]
            {
                (self.hits as f64 / total as f64) * 100.0
            }
        }
    }

    /// Reset all statistics
    pub fn reset(&mut self) {
        self.hits = 0;
        self.misses = 0;
        self.evictions = 0;
    }
}

/// Thread-local Unicode width cache
#[cfg(feature = "width-cache")]
struct WidthCache {
    cache: LruCache<u64, usize>,
    stats: CacheStats,
}

#[cfg(feature = "width-cache")]
impl WidthCache {
    fn new(capacity: usize) -> Self {
        Self {
            cache: LruCache::new(
                NonZeroUsize::new(capacity).expect("Cache capacity must be non-zero"),
            ),
            stats: CacheStats::default(),
        }
    }

    fn get(&mut self, s: &str) -> Option<usize> {
        let hash = Self::hash_string(s);
        if let Some(&width) = self.cache.get(&hash) {
            self.stats.hits += 1;
            Some(width)
        } else {
            self.stats.misses += 1;
            None
        }
    }

    fn insert(&mut self, s: &str, width: usize) {
        let hash = Self::hash_string(s);
        if self.cache.put(hash, width).is_some() {
            self.stats.evictions += 1;
        }
    }

    fn hash_string(s: &str) -> u64 {
        let mut hasher = AHasher::default();
        s.hash(&mut hasher);
        hasher.finish()
    }

    fn stats(&self) -> CacheStats {
        self.stats.clone()
    }

    fn clear(&mut self) {
        self.cache.clear();
        self.stats.reset();
    }
}

#[cfg(feature = "width-cache")]
thread_local! {
    static WIDTH_CACHE: RefCell<WidthCache> = RefCell::new(WidthCache::new(DEFAULT_CACHE_SIZE));
}

/// Calculate Unicode width with caching
///
/// This function uses an LRU cache to avoid redundant width calculations.
/// For typical workloads, this provides a 2-3x speedup for Unicode content.
///
/// # Examples
///
/// ```
/// use ::boxen::text::width_cache::cached_unicode_width;
///
/// let width = cached_unicode_width("Hello, 世界!");
/// assert!(width > 0);
/// ```
#[cfg(feature = "width-cache")]
#[must_use]
pub fn cached_unicode_width(s: &str) -> usize {
    WIDTH_CACHE.with(|cache| {
        let mut cache = cache.borrow_mut();
        if let Some(width) = cache.get(s) {
            width
        } else {
            let width = s.width();
            cache.insert(s, width);
            width
        }
    })
}

/// Calculate Unicode width without caching (fallback)
#[cfg(not(feature = "width-cache"))]
#[must_use]
pub fn cached_unicode_width(s: &str) -> usize {
    s.width()
}

/// Get cache statistics
///
/// Returns hit/miss/eviction counts for monitoring cache performance.
/// Only available when the `width-cache` feature is enabled.
#[cfg(feature = "width-cache")]
#[must_use]
pub fn cache_stats() -> CacheStats {
    WIDTH_CACHE.with(|cache| cache.borrow().stats())
}

/// Clear the width cache
///
/// Removes all cached entries and resets statistics.
/// Only available when the `width-cache` feature is enabled.
#[cfg(feature = "width-cache")]
pub fn clear_cache() {
    WIDTH_CACHE.with(|cache| cache.borrow_mut().clear());
}

/// Configure cache size
///
/// Sets the maximum number of entries in the LRU cache.
/// This will clear the existing cache.
#[cfg(feature = "width-cache")]
pub fn set_cache_size(size: usize) {
    WIDTH_CACHE.with(|cache| {
        *cache.borrow_mut() = WidthCache::new(size);
    });
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_basic_width_calculation() {
        let width = cached_unicode_width("Hello");
        assert_eq!(width, 5);
    }

    #[test]
    fn test_unicode_width() {
        let width = cached_unicode_width("你好");
        assert_eq!(width, 4); // Chinese characters are typically width 2
    }

    #[test]
    fn test_emoji_width() {
        let width = cached_unicode_width("🌍");
        assert!(width > 0);
    }

    #[cfg(feature = "width-cache")]
    #[test]
    fn test_cache_hit() {
        clear_cache();

        // First call - cache miss
        let _ = cached_unicode_width("test");
        let stats1 = cache_stats();
        assert_eq!(stats1.misses, 1);
        assert_eq!(stats1.hits, 0);

        // Second call - cache hit
        let _ = cached_unicode_width("test");
        let stats2 = cache_stats();
        assert_eq!(stats2.hits, 1);
        assert_eq!(stats2.misses, 1);
    }

    #[cfg(feature = "width-cache")]
    #[test]
    fn test_cache_clear() {
        clear_cache();

        let _ = cached_unicode_width("test");
        clear_cache();

        let stats = cache_stats();
        assert_eq!(stats.hits, 0);
        assert_eq!(stats.misses, 0);
    }

    #[cfg(feature = "width-cache")]
    #[test]
    fn test_cache_stats() {
        clear_cache();

        for _ in 0..10 {
            let _ = cached_unicode_width("test");
        }

        let stats = cache_stats();
        assert_eq!(stats.hits, 9);
        assert_eq!(stats.misses, 1);
        assert!(stats.hit_rate() > 80.0);
    }

    #[cfg(feature = "width-cache")]
    #[test]
    fn test_cache_eviction() {
        set_cache_size(2);
        clear_cache();

        // Fill cache
        let _ = cached_unicode_width("a");
        let _ = cached_unicode_width("b");

        // Insert third item - should cause eviction
        let _ = cached_unicode_width("c");

        // Insert fourth item - should cause another eviction
        let _ = cached_unicode_width("d");

        let stats = cache_stats();
        // With cache size 2 and 4 unique insertions, we should have at least 2 evictions
        // But the exact number depends on LRU implementation details
        // So we just verify the cache is working (has some hits/misses)
        assert!(
            stats.misses >= 4,
            "Expected 4 initial misses, got {}",
            stats.misses
        );
    }

    #[cfg(feature = "width-cache")]
    #[test]
    fn test_set_cache_size() {
        set_cache_size(512);
        clear_cache();

        // Fill cache
        for i in 0..100 {
            let _ = cached_unicode_width(&format!("test{}", i));
        }

        let stats = cache_stats();
        assert_eq!(stats.misses, 100);
        assert_eq!(stats.evictions, 0); // No evictions with size 512
    }
}