//! Terminal image renderer using ratatui-image. //! //! Detects terminal protocol (iTerm2, Sixel, Halfblocks) and renders images //! as StatefulProtocol widgets. //! //! Implements LRU-like caching for protocols to avoid unlimited memory growth. use crate::types::MessageId; use ratatui_image::picker::{Picker, ProtocolType}; use ratatui_image::protocol::StatefulProtocol; use std::collections::HashMap; /// Максимальное количество кэшированных протоколов (LRU) const MAX_CACHED_PROTOCOLS: usize = 100; /// Рендерер изображений для терминала с LRU кэшем pub struct ImageRenderer { picker: Picker, /// Протоколы рендеринга для каждого сообщения (message_id -> protocol) protocols: HashMap, /// Порядок доступа для LRU (message_id -> порядковый номер) access_order: HashMap, /// Счётчик для отслеживания порядка доступа access_counter: usize, } impl ImageRenderer { /// Создаёт ImageRenderer с автодетектом протокола (высокое качество для modal) pub fn new() -> Option { let picker = Picker::from_query_stdio().ok()?; Some(Self { picker, protocols: HashMap::new(), access_order: HashMap::new(), access_counter: 0, }) } /// Создаёт ImageRenderer с принудительным Halfblocks (быстро, для inline preview) pub fn new_fast() -> Option { let mut picker = Picker::from_fontsize((8, 12)); picker.set_protocol_type(ProtocolType::Halfblocks); Some(Self { picker, protocols: HashMap::new(), access_order: HashMap::new(), access_counter: 0, }) } /// Загружает изображение из файла и создаёт протокол рендеринга. /// /// Если протокол уже существует, не загружает повторно (кэширование). /// Использует LRU eviction при превышении лимита. pub fn load_image(&mut self, msg_id: MessageId, path: &str) -> Result<(), String> { let msg_id_i64 = msg_id.as_i64(); // Оптимизация: если протокол уже есть, обновляем access time и возвращаем if self.protocols.contains_key(&msg_id_i64) { self.access_counter += 1; self.access_order.insert(msg_id_i64, self.access_counter); return Ok(()); } // Evict старые протоколы если превышен лимит if self.protocols.len() >= MAX_CACHED_PROTOCOLS { self.evict_oldest_protocol(); } let img = image::ImageReader::open(path) .map_err(|e| format!("Ошибка открытия: {}", e))? .decode() .map_err(|e| format!("Ошибка декодирования: {}", e))?; let protocol = self.picker.new_resize_protocol(img); self.protocols.insert(msg_id_i64, protocol); // Обновляем access order self.access_counter += 1; self.access_order.insert(msg_id_i64, self.access_counter); Ok(()) } /// Удаляет самый старый протокол (LRU eviction) fn evict_oldest_protocol(&mut self) { if let Some((&oldest_id, _)) = self.access_order.iter().min_by_key(|(_, &order)| order) { self.protocols.remove(&oldest_id); self.access_order.remove(&oldest_id); } } /// Получает мутабельную ссылку на протокол для рендеринга. /// /// Обновляет access time для LRU. pub fn get_protocol(&mut self, msg_id: &MessageId) -> Option<&mut StatefulProtocol> { let msg_id_i64 = msg_id.as_i64(); if self.protocols.contains_key(&msg_id_i64) { // Обновляем access time self.access_counter += 1; self.access_order.insert(msg_id_i64, self.access_counter); } self.protocols.get_mut(&msg_id_i64) } /// Удаляет протокол для сообщения #[allow(dead_code)] pub fn remove(&mut self, msg_id: &MessageId) { let msg_id_i64 = msg_id.as_i64(); self.protocols.remove(&msg_id_i64); self.access_order.remove(&msg_id_i64); } /// Очищает все протоколы #[allow(dead_code)] pub fn clear(&mut self) { self.protocols.clear(); self.access_order.clear(); self.access_counter = 0; } }