1use std::any::Any;
6use std::sync::Mutex;
7
8use super::component::Component;
9use super::terminal_image::{
10 allocate_image_id, get_capabilities, get_image_dimensions, render_image, ImageProtocol,
11 ImageRenderOptions,
12};
13use crate::utils::visible_width;
14
15#[derive(Debug, Clone)]
17pub struct ImageTheme {
18 pub placeholder_bg: &'static str,
20 pub border_color: &'static str,
22 pub alt_text_color: fn(&str) -> String,
24}
25
26impl Default for ImageTheme {
27 fn default() -> Self {
28 Self {
29 placeholder_bg: "\x1b[48;5;238m", border_color: "\x1b[38;5;244m", alt_text_color: |s| format!("\x1b[38;5;250m{}\x1b[0m", s),
32 }
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct ImageOptions {
39 pub width: Option<u16>,
41 pub height: Option<u16>,
43 pub preserve_aspect: bool,
45 pub alt_text: Option<String>,
47 pub title: Option<String>,
49}
50
51impl Default for ImageOptions {
52 fn default() -> Self {
53 Self {
54 width: None,
55 height: None,
56 preserve_aspect: true,
57 alt_text: None,
58 title: None,
59 }
60 }
61}
62
63pub struct Image {
65 data: Mutex<Option<Vec<u8>>>,
66 url: Mutex<Option<String>>,
67 options: ImageOptions,
68 theme: ImageTheme,
69 image_id: Mutex<Option<u32>>,
70 cached_render: Mutex<Option<(usize, String)>>,
71 inline_visible: Mutex<bool>,
72}
73
74impl Image {
75 pub fn new() -> Self {
77 Self {
78 data: Mutex::new(None),
79 url: Mutex::new(None),
80 options: ImageOptions::default(),
81 theme: ImageTheme::default(),
82 image_id: Mutex::new(None),
83 cached_render: Mutex::new(None),
84 inline_visible: Mutex::new(true),
85 }
86 }
87
88 pub fn from_data(data: Vec<u8>, options: ImageOptions) -> Self {
90 let id = allocate_image_id();
91 Self {
92 data: Mutex::new(Some(data)),
93 url: Mutex::new(None),
94 options,
95 theme: ImageTheme::default(),
96 image_id: Mutex::new(Some(id)),
97 cached_render: Mutex::new(None),
98 inline_visible: Mutex::new(true),
99 }
100 }
101
102 pub fn from_url(url: &str, options: ImageOptions) -> Self {
104 Self {
105 data: Mutex::new(None),
106 url: Mutex::new(Some(url.to_string())),
107 options,
108 theme: ImageTheme::default(),
109 image_id: Mutex::new(None),
110 cached_render: Mutex::new(None),
111 inline_visible: Mutex::new(true),
112 }
113 }
114
115 pub fn set_data(&self, data: Vec<u8>) {
117 if let Ok(mut d) = self.data.lock() {
118 *d = Some(data);
119 }
120 if let Ok(mut id) = self.image_id.lock() {
121 *id = Some(allocate_image_id());
122 }
123 if let Ok(mut cached) = self.cached_render.lock() {
124 *cached = None;
125 }
126 }
127
128 pub fn set_url(&self, url: &str) {
130 if let Ok(mut u) = self.url.lock() {
131 *u = Some(url.to_string());
132 }
133 if let Ok(mut cached) = self.cached_render.lock() {
134 *cached = None;
135 }
136 }
137
138 pub fn set_options(&mut self, options: ImageOptions) {
140 self.options = options;
141 if let Ok(mut cached) = self.cached_render.lock() {
142 *cached = None;
143 }
144 }
145
146 pub fn set_alt_text(&mut self, alt_text: &str) {
148 self.options.alt_text = Some(alt_text.to_string());
149 }
150
151 pub fn set_theme(&mut self, theme: ImageTheme) {
153 self.theme = theme;
154 if let Ok(mut cached) = self.cached_render.lock() {
155 *cached = None;
156 }
157 }
158
159 pub fn is_loaded(&self) -> bool {
161 self.data.lock().map(|d| d.is_some()).unwrap_or(false)
162 || self.url.lock().map(|u| u.is_some()).unwrap_or(false)
163 }
164
165 pub fn get_dimensions(&self) -> Option<(u32, u32)> {
167 let data = self.data.lock().ok()?;
168 let d = data.as_ref()?;
169 let dims = get_image_dimensions(d)?;
170 Some((dims.width, dims.height))
171 }
172
173 fn render_image(&self, width: usize) -> String {
175 if width == 0 {
176 return String::new();
177 }
178 if !*self.inline_visible.lock().unwrap() {
179 return crate::utils::truncate_to_width("[Image]", width, "");
180 }
181 let caps = get_capabilities();
182
183 if let Ok(cached) = self.cached_render.lock() {
185 if let Some((cached_width, rendered)) = cached.as_ref() {
186 if *cached_width == width {
187 return rendered.clone();
188 }
189 }
190 }
191
192 let (img_width, img_height) = if let Some((w, h)) = self.get_dimensions() {
194 let cell_dims = super::terminal_image::get_cell_dimensions();
196 let cell_width = self
197 .options
198 .width
199 .unwrap_or_else(|| {
200 ((w as f32 / cell_dims.width.max(1) as f32).ceil() as u16).min(60)
201 })
202 .min(width.saturating_sub(2).max(1).min(u16::MAX as usize) as u16)
203 .max(1);
204 let cell_height = self
205 .options
206 .height
207 .unwrap_or_else(|| {
208 ((h as f64 / w.max(1) as f64) * cell_width as f64 * cell_dims.width as f64
209 / cell_dims.height.max(1) as f64)
210 .ceil() as u16
211 })
212 .max(1);
213 let max_height = self
214 .options
215 .height
216 .unwrap_or_else(|| {
217 (cell_width as f64 * cell_dims.width as f64 / cell_dims.height.max(1) as f64)
218 .ceil() as u16
219 })
220 .max(1);
221 if self.options.preserve_aspect && cell_height > max_height {
222 (
223 ((cell_width as f64 * max_height as f64 / cell_height as f64).floor() as u16)
224 .max(1),
225 max_height,
226 )
227 } else {
228 (cell_width, cell_height)
229 }
230 } else {
231 (
232 self.options.width.unwrap_or(width as u16),
233 self.options.height.unwrap_or(5),
234 )
235 };
236
237 let result = if caps.protocol == ImageProtocol::None {
239 crate::utils::truncate_to_width(
241 self.options.alt_text.as_deref().unwrap_or("[Image]"),
242 width,
243 "",
244 )
245 } else if let Ok(data) = self.data.lock() {
246 if let Some(ref img_data) = *data {
247 let id = self.image_id.lock().ok().and_then(|id| *id);
248 let options = ImageRenderOptions {
249 width: Some(img_width),
250 height: Some(img_height),
251 preserve_aspect: self.options.preserve_aspect,
252 x: 0,
253 y: 0,
254 z_index: None,
255 id,
256 };
257 let sequence = render_image(img_data, &options);
260 let rows = img_height.saturating_sub(1) as usize;
261 if caps.protocol == ImageProtocol::Kitty {
262 format!("{sequence}{}", "\n".repeat(rows))
263 } else {
264 let up = if rows > 0 {
265 format!("\x1b[{rows}A")
266 } else {
267 String::new()
268 };
269 format!("{}{up}{sequence}", "\n".repeat(rows))
270 }
271 } else {
272 self.render_placeholder(img_width as usize, img_height as usize, width)
273 }
274 } else {
275 self.render_placeholder(img_width as usize, img_height as usize, width)
276 };
277
278 if let Ok(mut cached) = self.cached_render.lock() {
280 *cached = Some((width, result.clone()));
281 }
282
283 result
284 }
285
286 pub fn set_inline_visible(&self, visible: bool) {
288 let mut current = self.inline_visible.lock().unwrap();
289 if *current == visible {
290 return;
291 }
292 *current = visible;
293 *self.cached_render.lock().unwrap() = None;
294 }
295
296 fn render_placeholder(&self, img_width: usize, img_height: usize, max_width: usize) -> String {
298 let width = img_width.min(max_width);
299 let height = img_height.max(3);
300
301 let border_h = "─".repeat(width.saturating_sub(2));
302 let inner_width = width.saturating_sub(2);
303
304 let mut lines = Vec::new();
305
306 lines.push(format!("{}╭{}╮\x1b[0m", self.theme.border_color, border_h));
308
309 let alt_text = self.options.alt_text.as_deref().unwrap_or("[Image]");
311 let title = self.options.title.as_deref().unwrap_or("");
312
313 let content_height = height.saturating_sub(2);
315 let alt_text_line = (self.theme.alt_text_color)(alt_text);
316 let title_line = if !title.is_empty() {
317 Some((self.theme.alt_text_color)(title))
318 } else {
319 None
320 };
321
322 for i in 0..content_height {
323 let content = if i == 0 {
324 if let Some(ref t) = title_line {
325 t.clone()
326 } else {
327 alt_text_line.clone()
328 }
329 } else if i == 1 && title_line.is_some() {
330 alt_text_line.clone()
331 } else {
332 " ".repeat(inner_width)
333 };
334
335 let content_padded = if visible_width(&content) < inner_width {
336 format!(
337 "{}{}",
338 content,
339 " ".repeat(inner_width - visible_width(&content))
340 )
341 } else {
342 crate::utils::truncate_to_width(&content, inner_width, "")
343 };
344
345 lines.push(format!(
346 "{}│{}{}\x1b[0m│\x1b[0m",
347 self.theme.border_color, self.theme.placeholder_bg, content_padded
348 ));
349 }
350
351 lines.push(format!("{}╰{}╯\x1b[0m", self.theme.border_color, border_h));
353
354 lines.join("\n")
355 }
356}
357
358impl Default for Image {
359 fn default() -> Self {
360 Self::new()
361 }
362}
363
364impl Component for Image {
365 fn render(&self, width: usize) -> Vec<String> {
366 let rendered = self.render_image(width);
367 rendered.split('\n').map(|s| s.to_string()).collect()
368 }
369
370 fn invalidate(&self) {
371 if let Ok(mut cached) = self.cached_render.lock() {
372 *cached = None;
373 }
374 }
375
376 fn as_any(&self) -> &dyn Any {
377 self
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn image_visibility_retains_placeholder_and_resizing_rebuilds_layout() {
387 let image = Image::new();
388 image.set_inline_visible(false);
389 assert_eq!(image.render(80), vec!["[Image]"]);
390 assert_eq!(image.render(3), vec!["[Im"]);
391 assert_eq!(image.render(0), vec![""]);
392 image.set_inline_visible(true);
393 let wide = image.render(80);
394 let narrow = image.render(3);
395 assert_ne!(wide, narrow);
396 assert!(narrow.iter().all(|line| visible_width(line) <= 3));
397 }
398
399 #[test]
400 fn test_image_new() {
401 let image = Image::new();
402 assert!(!image.is_loaded());
403 }
404
405 #[test]
406 fn test_image_from_data() {
407 let data = vec![0x89, 0x50, 0x4E, 0x47]; let image = Image::from_data(data, ImageOptions::default());
409 assert!(image.is_loaded());
410 }
411
412 #[test]
413 fn test_image_placeholder() {
414 let image = Image::new();
415 let lines = image.render(20);
416 assert!(!lines.is_empty());
417 }
418}