1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
use std::io::{stdout, Write};
use anyhow::{Context, Result};
use crossterm::{
cursor::{MoveTo, RestorePosition, SavePosition},
execute,
terminal::{disable_raw_mode, enable_raw_mode},
};
use ratatui::layout::Rect;
use crate::{common::CHAFA, io::ImageDisplayer};
use crate::{io::execute_and_capture_output_with_path, modes::DisplayedImage};
/// Holds the path of the image and a rect surrounding its display position.
/// It's used to:
/// - avoid drawing the same image over and over,
/// - know where to draw the new image,
/// - know where to erase the last image.
#[derive(Debug)]
struct PathRect {
path: String,
rect: Rect,
}
impl PathRect {
fn new(path: String, rect: Rect) -> Self {
Self { path, rect }
}
/// true iff the displayed image path and its rect haven't changed
fn is_same(&self, path: &str, rect: Rect) -> bool {
self.path == path && self.rect == rect
}
}
/// Which image was displayed, where on the screen and is it displayed ?
#[derive(Default, Debug)]
pub struct Chafa {
last_displayed: Option<PathRect>,
is_displaying: bool,
}
impl ImageDisplayer for Chafa {
/// Draws the image to the terminal using [chafa](<https://hpjansson.org/chafa/>).
///
/// The drawing is done using the first method supported by the terminal (iterm2, kitty, sixel or symbols).
/// It requires a string to be "written" to the terminal itself.
fn draw(&mut self, image: &DisplayedImage, rect: Rect) -> Result<()> {
let path = &image.selected_path();
if self.image_can_be_reused(path, rect) {
return Ok(());
}
let image_string = Self::encode_chafa(path, rect)?;
let image_encoded = image_string.as_bytes();
Self::write_image_to_term(image_encoded, rect)?;
self.is_displaying = true;
self.last_displayed = Some(PathRect::new(path.to_string(), rect));
Ok(())
}
/// Clear the last displayed image.
/// Alias to clear_all.
///
/// If an image is currently displayed, write lines of " " in all its rect.
fn clear(&mut self, _: &DisplayedImage) -> Result<()> {
self.clear_all()
}
/// Clear the last displayed image.
/// If an image is currently displayed, write lines of " " in all its rect.
fn clear_all(&mut self) -> Result<()> {
if let Some(PathRect { path: _, rect }) = self.last_displayed {
Self::clear_image_rect(rect)?;
}
self.is_displaying = false;
self.last_displayed = None;
Ok(())
}
}
impl Chafa {
/// True iff the image already drawned can be reused.
/// Two conditions must be true:
/// - we are displaying something (is_displaying is true)
/// - the image itself and its position haven't changed (path and rect haven't changed)
fn image_can_be_reused<P>(&self, path: P, rect: Rect) -> bool
where
P: AsRef<str>,
{
if !self.is_displaying {
return false;
}
if let Some(path_rect) = &self.last_displayed {
path_rect.is_same(path.as_ref(), rect)
} else {
false
}
}
/// Encode an image to a string using iterm2 inline image protocol.
fn encode_chafa<P>(path: P, rect: Rect) -> Result<String>
where
P: AsRef<str>,
{
Self::write_chafa(path.as_ref(), rect.width, rect.height)
}
/// To draw an image on the terminal using Inline Image Protocol,
/// We must :
/// - disable raw mode,
/// - move to the position,
/// - write the encoded bytes to stdout,
/// - enable raw mode.
///
/// Heavily inspired by Yazi.
fn write_image_to_term(encoded_image: &[u8], rect: Rect) -> std::io::Result<()> {
disable_raw_mode()?;
execute!(stdout(), MoveTo(rect.x, rect.y))?;
stdout().write_all(encoded_image)?;
enable_raw_mode()
}
/// Clear the rect where the last image were drawned.
/// Simply write `height` empty lines of length `width`.
fn clear_image_rect(rect: Rect) -> std::io::Result<()> {
let empty_line = " ".repeat(rect.width as usize);
let empty_bytes = empty_line.as_bytes();
disable_raw_mode()?;
execute!(stdout(), SavePosition)?;
for y in rect.top()..rect.bottom() {
execute!(stdout(), MoveTo(rect.x, y))?;
stdout().write_all(empty_bytes)?;
}
execute!(stdout(), RestorePosition)?;
enable_raw_mode()
}
/// Creates the chafa string. We force a view-size of the surrounding rect and ensure "relative" is on. It allows
/// the image to be displayed properly in its position.
///
/// The resizing must be done by the terminal emulator itself.
fn write_chafa(path: &str, width: u16, height: u16) -> Result<String> {
let output = execute_and_capture_output_with_path(
CHAFA,
std::path::Path::new(path)
.parent()
.context("no parent of image path")?,
&[
"--view-size",
&format!("{width}x{height}"),
"--relative",
"on",
path,
],
)?;
Ok(output)
}
}