Files
telegram-tui/src/media/image_renderer.rs
Mikhail Kilin 3b7ef41cae
Some checks failed
ci/woodpecker/pr/check Pipeline was successful
CI / Check (pull_request) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Build (windows-latest) (pull_request) Has been cancelled
fix: resolve all 40 clippy warnings (dead_code, unused_imports, lints)
- Add #[allow(unused_imports)] on pub re-exports used only by lib/tests
- Add #[allow(dead_code)] on public API items unused in binary target
- Fix collapsible_if, redundant_closure, unnecessary_map_or in main.rs
- Prefix unused test variables with underscore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:50:18 +03:00

126 lines
4.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<i64, StatefulProtocol>,
/// Порядок доступа для LRU (message_id -> порядковый номер)
access_order: HashMap<i64, usize>,
/// Счётчик для отслеживания порядка доступа
access_counter: usize,
}
impl ImageRenderer {
/// Создаёт ImageRenderer с автодетектом протокола (высокое качество для modal)
pub fn new() -> Option<Self> {
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<Self> {
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;
}
}