1use std::io::{stdout, Write};
2
3use anyhow::{Context, Result};
4use crossterm::{
5 cursor::{MoveTo, RestorePosition, SavePosition},
6 execute,
7 terminal::{disable_raw_mode, enable_raw_mode},
8};
9use ratatui::layout::Rect;
10
11use crate::{common::CHAFA, io::ImageDisplayer};
12use crate::{io::execute_and_capture_output_with_path, modes::DisplayedImage};
13
14#[derive(Debug)]
20struct PathRect {
21 path: String,
22 rect: Rect,
23}
24
25impl PathRect {
26 fn new(path: String, rect: Rect) -> Self {
27 Self { path, rect }
28 }
29
30 fn is_same(&self, path: &str, rect: Rect) -> bool {
32 self.path == path && self.rect == rect
33 }
34}
35
36#[derive(Default, Debug)]
38pub struct Chafa {
39 last_displayed: Option<PathRect>,
40 is_displaying: bool,
41}
42
43impl ImageDisplayer for Chafa {
44 fn draw(&mut self, image: &DisplayedImage, rect: Rect) -> Result<()> {
49 let path = &image.selected_path();
50 if self.image_can_be_reused(path, rect) {
51 return Ok(());
52 }
53 let image_string = Self::encode_chafa(path, rect)?;
54 let image_encoded = image_string.as_bytes();
55 Self::write_image_to_term(image_encoded, rect)?;
56 self.is_displaying = true;
57 self.last_displayed = Some(PathRect::new(path.to_string(), rect));
58 Ok(())
59 }
60
61 fn clear(&mut self, _: &DisplayedImage) -> Result<()> {
66 self.clear_all()
67 }
68
69 fn clear_all(&mut self) -> Result<()> {
72 if let Some(PathRect { path: _, rect }) = self.last_displayed {
73 Self::clear_image_rect(rect)?;
74 }
75 self.is_displaying = false;
76 self.last_displayed = None;
77 Ok(())
78 }
79}
80
81impl Chafa {
82 fn image_can_be_reused<P>(&self, path: P, rect: Rect) -> bool
87 where
88 P: AsRef<str>,
89 {
90 if !self.is_displaying {
91 return false;
92 }
93 if let Some(path_rect) = &self.last_displayed {
94 path_rect.is_same(path.as_ref(), rect)
95 } else {
96 false
97 }
98 }
99
100 fn encode_chafa<P>(path: P, rect: Rect) -> Result<String>
102 where
103 P: AsRef<str>,
104 {
105 Self::write_chafa(path.as_ref(), rect.width, rect.height)
106 }
107
108 fn write_image_to_term(encoded_image: &[u8], rect: Rect) -> std::io::Result<()> {
117 disable_raw_mode()?;
118 execute!(stdout(), MoveTo(rect.x, rect.y))?;
119 stdout().write_all(encoded_image)?;
120 enable_raw_mode()
121 }
122
123 fn clear_image_rect(rect: Rect) -> std::io::Result<()> {
126 let empty_line = " ".repeat(rect.width as usize);
127 let empty_bytes = empty_line.as_bytes();
128 disable_raw_mode()?;
129 execute!(stdout(), SavePosition)?;
130 for y in rect.top()..rect.bottom() {
131 execute!(stdout(), MoveTo(rect.x, y))?;
132 stdout().write_all(empty_bytes)?;
133 }
134 execute!(stdout(), RestorePosition)?;
135 enable_raw_mode()
136 }
137
138 fn write_chafa(path: &str, width: u16, height: u16) -> Result<String> {
143 let output = execute_and_capture_output_with_path(
144 CHAFA,
145 std::path::Path::new(path)
146 .parent()
147 .context("no parent of image path")?,
148 &[
149 "--view-size",
150 &format!("{width}x{height}"),
151 "--relative",
152 "on",
153 path,
154 ],
155 )?;
156
157 Ok(output)
158 }
159}