fm/io/
inline_image_protocol.rs1use std::{
2 fmt::Write as FmtWrite,
3 fs::File,
4 io::{stdout, Read, Write},
5};
6
7use anyhow::Result;
8use base64::{
9 encoded_len as base64_encoded_len,
10 engine::{general_purpose::STANDARD, Config},
11 Engine,
12};
13use crossterm::{
14 cursor::{MoveTo, RestorePosition, SavePosition},
15 execute,
16 terminal::{disable_raw_mode, enable_raw_mode},
17};
18use ratatui::layout::Rect;
19
20use crate::io::ImageDisplayer;
21use crate::modes::DisplayedImage;
22
23#[derive(Debug)]
29struct PathRect {
30 path: String,
31 rect: Rect,
32}
33
34impl PathRect {
35 fn new(path: String, rect: Rect) -> Self {
36 Self { path, rect }
37 }
38
39 fn is_same(&self, path: &str, rect: Rect) -> bool {
41 self.path == path && self.rect == rect
42 }
43}
44
45#[derive(Default, Debug)]
47pub struct InlineImage {
48 last_displayed: Option<PathRect>,
49 is_displaying: bool,
50}
51
52impl ImageDisplayer for InlineImage {
53 fn draw(&mut self, image: &DisplayedImage, rect: Rect) -> Result<()> {
58 let path = &image.selected_path();
59 if self.image_can_be_reused(path, rect) {
60 return Ok(());
61 }
62 let image_string = Self::encode_to_string(path, rect)?;
63 let image_encoded = image_string.as_bytes();
64 Self::write_image_to_term(image_encoded, rect)?;
65 self.is_displaying = true;
66 self.last_displayed = Some(PathRect::new(path.to_string(), rect));
67 Ok(())
68 }
69
70 fn clear(&mut self, _: &DisplayedImage) -> Result<()> {
75 self.clear_all()
76 }
77
78 fn clear_all(&mut self) -> Result<()> {
81 if let Some(PathRect { path: _, rect }) = self.last_displayed {
82 Self::clear_image_rect(rect)?;
83 }
84 self.is_displaying = false;
85 self.last_displayed = None;
86 Ok(())
87 }
88}
89
90impl InlineImage {
91 fn image_can_be_reused<P>(&self, path: P, rect: Rect) -> bool
96 where
97 P: AsRef<str>,
98 {
99 if !self.is_displaying {
100 return false;
101 }
102 if let Some(path_rect) = &self.last_displayed {
103 path_rect.is_same(path.as_ref(), rect)
104 } else {
105 false
106 }
107 }
108
109 fn read_as_bytes<P>(path: P) -> std::io::Result<Vec<u8>>
111 where
112 P: AsRef<str>,
113 {
114 let mut f = File::open(path.as_ref())?;
115 let mut buf = Vec::new();
116 f.read_to_end(&mut buf)?;
117 Ok(buf)
118 }
119
120 fn encode_to_string<P>(path: P, rect: Rect) -> Result<String>
122 where
123 P: AsRef<str>,
124 {
125 Self::write_inline_image_string(
126 &Self::read_as_bytes(path)?,
127 rect.width.saturating_sub(1),
128 rect.height.saturating_sub(4),
129 )
130 }
131
132 fn write_image_to_term(encoded_image: &[u8], rect: Rect) -> std::io::Result<()> {
141 disable_raw_mode()?;
142 execute!(stdout(), MoveTo(rect.x, rect.y))?;
143 stdout().write_all(encoded_image)?;
144 enable_raw_mode()
145 }
146
147 fn clear_image_rect(rect: Rect) -> std::io::Result<()> {
150 let empty_line = " ".repeat(rect.width as usize);
151 let empty_bytes = empty_line.as_bytes();
152 disable_raw_mode()?;
153 execute!(stdout(), SavePosition)?;
154 for y in rect.top()..rect.bottom() {
155 execute!(stdout(), MoveTo(rect.x, y))?;
156 stdout().write_all(empty_bytes)?;
157 }
158 execute!(stdout(), RestorePosition)?;
159 enable_raw_mode()
160 }
161
162 fn write_inline_image_string(buffer: &[u8], width: u16, height: u16) -> Result<String> {
168 let mut string = String::with_capacity(Self::guess_string_capacity(buffer));
169 write!(
170 string,
171 "\x1b]1337;File=inline=1;size={size};width={width};height={height};doNotMoveCursor=1;preserveAspectRatio=1:",
172 size = buffer.len(),
173 )?;
174 STANDARD.encode_string(buffer, &mut string);
175 write!(string, "\u{0007}")?;
176 Ok(string)
177 }
178
179 fn guess_string_capacity(buffer: &[u8]) -> usize {
180 200 + base64_encoded_len(buffer.len(), STANDARD.config().encode_padding()).unwrap_or(0)
181 }
182}