#[cfg(feature = "terminal-cache")]
use std::cell::RefCell;
#[cfg(feature = "terminal-cache")]
use std::time::{Duration, Instant};
use terminal_size::{Height, Width, terminal_size};
#[cfg(feature = "terminal-cache")]
const DEFAULT_TTL_MS: u64 = 100;
#[cfg(feature = "terminal-cache")]
#[derive(Debug, Clone)]
struct CachedSize {
width: u16,
height: u16,
timestamp: Instant,
}
#[cfg(feature = "terminal-cache")]
impl CachedSize {
fn new(width: u16, height: u16) -> Self {
Self {
width,
height,
timestamp: Instant::now(),
}
}
fn is_expired(&self, ttl: Duration) -> bool {
self.timestamp.elapsed() > ttl
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: usize,
pub misses: usize,
pub expirations: usize,
pub invalidations: usize,
}
impl CacheStats {
#[must_use]
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
#[allow(clippy::cast_precision_loss)]
{
(self.hits as f64 / total as f64) * 100.0
}
}
}
pub fn reset(&mut self) {
self.hits = 0;
self.misses = 0;
self.expirations = 0;
self.invalidations = 0;
}
}
#[cfg(feature = "terminal-cache")]
struct TerminalSizeCache {
cached: Option<CachedSize>,
ttl: Duration,
stats: CacheStats,
}
#[cfg(feature = "terminal-cache")]
impl TerminalSizeCache {
fn new(ttl_ms: u64) -> Self {
Self {
cached: None,
ttl: Duration::from_millis(ttl_ms),
stats: CacheStats::default(),
}
}
fn get(&mut self) -> Option<(u16, u16)> {
if let Some(ref cached) = self.cached {
if !cached.is_expired(self.ttl) {
self.stats.hits += 1;
return Some((cached.width, cached.height));
}
self.stats.expirations += 1;
self.cached = None;
}
self.stats.misses += 1;
None
}
fn set(&mut self, width: u16, height: u16) {
self.cached = Some(CachedSize::new(width, height));
}
fn invalidate(&mut self) {
if self.cached.is_some() {
self.stats.invalidations += 1;
self.cached = None;
}
}
fn set_ttl(&mut self, ttl_ms: u64) {
self.ttl = Duration::from_millis(ttl_ms);
}
fn stats(&self) -> CacheStats {
self.stats.clone()
}
fn clear(&mut self) {
self.cached = None;
self.stats.reset();
}
}
#[cfg(feature = "terminal-cache")]
thread_local! {
static SIZE_CACHE: RefCell<TerminalSizeCache> = RefCell::new(TerminalSizeCache::new(DEFAULT_TTL_MS));
}
#[cfg(feature = "terminal-cache")]
#[must_use]
pub fn cached_terminal_size() -> Option<(u16, u16)> {
SIZE_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(size) = cache.get() {
return Some(size);
}
if let Some((Width(w), Height(h))) = terminal_size() {
cache.set(w, h);
Some((w, h))
} else {
None
}
})
}
#[cfg(not(feature = "terminal-cache"))]
#[must_use]
pub fn cached_terminal_size() -> Option<(u16, u16)> {
terminal_size().map(|(Width(w), Height(h))| (w, h))
}
#[cfg(feature = "terminal-cache")]
pub fn invalidate_cache() {
SIZE_CACHE.with(|cache| {
cache.borrow_mut().invalidate();
});
}
#[cfg(feature = "terminal-cache")]
#[must_use]
pub fn cache_stats() -> CacheStats {
SIZE_CACHE.with(|cache| cache.borrow().stats())
}
#[cfg(feature = "terminal-cache")]
pub fn clear_cache() {
SIZE_CACHE.with(|cache| cache.borrow_mut().clear());
}
#[cfg(feature = "terminal-cache")]
pub fn set_cache_ttl(ttl_ms: u64) {
SIZE_CACHE.with(|cache| {
cache.borrow_mut().set_ttl(ttl_ms);
});
}
#[cfg(all(feature = "terminal-cache", unix))]
pub fn setup_sigwinch_handler() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
static HANDLER_INSTALLED: AtomicBool = AtomicBool::new(false);
if HANDLER_INSTALLED.swap(true, Ordering::SeqCst) {
return;
}
let term = Arc::new(AtomicBool::new(false));
signal_hook::flag::register(signal_hook::consts::SIGWINCH, Arc::clone(&term))
.expect("Failed to register SIGWINCH handler");
std::thread::spawn(move || {
loop {
if term.load(Ordering::Relaxed) {
invalidate_cache();
term.store(false, Ordering::Relaxed);
}
std::thread::sleep(Duration::from_millis(10));
}
});
}
#[cfg(all(feature = "terminal-cache", not(unix)))]
pub fn setup_sigwinch_handler() {
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_terminal_size() {
let size = cached_terminal_size();
if let Some((w, h)) = size {
assert!(w > 0);
assert!(h > 0);
}
}
#[cfg(feature = "terminal-cache")]
#[test]
fn test_cache_hit() {
clear_cache();
let _ = cached_terminal_size();
let stats1 = cache_stats();
assert_eq!(stats1.misses, 1);
let _ = cached_terminal_size();
let stats2 = cache_stats();
if cached_terminal_size().is_some() {
assert_eq!(stats2.hits, 1);
}
}
#[cfg(feature = "terminal-cache")]
#[test]
fn test_cache_invalidation() {
clear_cache();
let _ = cached_terminal_size();
invalidate_cache();
let stats = cache_stats();
if cached_terminal_size().is_some() {
assert_eq!(stats.invalidations, 1);
}
}
#[cfg(feature = "terminal-cache")]
#[test]
fn test_cache_clear() {
clear_cache();
let _ = cached_terminal_size();
clear_cache();
let stats = cache_stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[cfg(feature = "terminal-cache")]
#[test]
fn test_set_ttl() {
set_cache_ttl(50);
clear_cache();
let _ = cached_terminal_size();
std::thread::sleep(Duration::from_millis(60));
let _ = cached_terminal_size();
let stats = cache_stats();
if cached_terminal_size().is_some() {
assert!(stats.expirations > 0);
}
}
#[cfg(feature = "terminal-cache")]
#[test]
fn test_cache_stats() {
clear_cache();
for _ in 0..10 {
let _ = cached_terminal_size();
}
let stats = cache_stats();
if cached_terminal_size().is_some() {
assert!(stats.hits > 0);
assert_eq!(stats.misses, 1);
assert!(stats.hit_rate() > 80.0);
}
}
}