1use std::env::var;
35use std::fmt;
36use std::io::Write;
37use std::process::{Child, Command, Stdio};
38
39use anyhow::{Context, Result};
40use ratatui::layout::Rect;
41use serde::Serialize;
42use serde_json::Result as ResultSerdeJson;
43
44use crate::common::UEBERZUG;
45use crate::io::ImageDisplayer;
46use crate::modes::{DisplayedImage, Quote};
47
48pub fn user_has_x11() -> bool {
57 if var("DISPLAY").is_err() {
58 return false;
59 }
60
61 Command::new("xset")
62 .arg("q")
63 .stdin(Stdio::null())
64 .stdout(Stdio::null())
65 .stderr(Stdio::null())
66 .status()
67 .map(|s| s.success())
68 .unwrap_or(false)
69}
70
71pub struct Ueberzug {
76 driver: Child,
77 last_displayed: Option<String>,
78 is_displaying: bool,
79}
80
81impl Default for Ueberzug {
82 fn default() -> Self {
85 Self {
86 driver: Self::spawn_ueberzug().unwrap(),
87 last_displayed: None,
88 is_displaying: false,
89 }
90 }
91}
92
93impl ImageDisplayer for Ueberzug {
94 fn draw(&mut self, image: &DisplayedImage, rect: Rect) -> Result<()> {
96 let path = image.selected_path().quote()?;
97
98 if self.is_the_same_image(&path) {
99 Ok(())
100 } else {
101 self.clear(image)?;
102 self.is_displaying = true;
103 self.last_displayed = Some(path.to_string());
104 self.run(&UeConf::add_json(image, rect)?)
105 }
106 }
107
108 fn clear(&mut self, _: &DisplayedImage) -> Result<()> {
111 if self.is_displaying {
112 self.clear_internal()
113 } else {
114 Ok(())
115 }
116 }
117
118 fn clear_all(&mut self) -> Result<()> {
120 self.is_displaying = false;
121 self.last_displayed = None;
122 self.driver = Self::spawn_ueberzug()?;
123 Ok(())
124 }
125}
126
127impl Ueberzug {
128 fn clear_internal(&mut self) -> Result<()> {
129 self.is_displaying = false;
130 self.last_displayed = None;
131 self.run(&UeConf::remove_json("fm_tui")?)
132 }
133 fn is_the_same_image(&mut self, new: &str) -> bool {
135 let Some(last) = &self.last_displayed else {
136 return false;
137 };
138 last == new
139 }
140
141 fn spawn_ueberzug() -> std::io::Result<Child> {
142 std::process::Command::new(UEBERZUG)
143 .arg("layer")
144 .arg("--silent")
145 .stdin(Stdio::piped())
146 .stdout(Stdio::piped())
147 .stderr(Stdio::piped())
148 .spawn()
149 }
150
151 fn run(&mut self, cmd: &str) -> Result<()> {
152 self.driver
153 .stdin
154 .as_mut()
155 .context("stdin shouldn't be None")?
156 .write_all(cmd.as_bytes())?;
157 self.driver
158 .stdin
159 .as_mut()
160 .context("stdin shouldn't be None")?
161 .write_all(b"\n")?;
162 Ok(())
163 }
164}
165
166#[derive(Serialize)]
168pub enum Actions {
169 #[serde(rename(serialize = "add"))]
170 Add,
171 #[serde(rename(serialize = "remove"))]
172 Remove,
173}
174
175impl fmt::Display for Actions {
176 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
177 match self {
178 Actions::Add => write!(f, "add"),
179 Actions::Remove => write!(f, "remove"),
180 }
181 }
182}
183#[derive(Clone, Copy, Serialize)]
185pub enum Scalers {
186 #[serde(rename(serialize = "crop"))]
187 Crop,
188 #[serde(rename(serialize = "distort"))]
189 Distort,
190 #[serde(rename(serialize = "fit_contain"))]
191 FitContain,
192 #[serde(rename(serialize = "contain"))]
193 Contain,
194 #[serde(rename(serialize = "forced_cover"))]
195 ForcedCover,
196 #[serde(rename(serialize = "cover"))]
197 Cover,
198}
199
200impl fmt::Display for Scalers {
201 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
202 match self {
203 Scalers::Contain => write!(f, "contain"),
204 Scalers::Cover => write!(f, "cover"),
205 Scalers::Crop => write!(f, "crop"),
206 Scalers::Distort => write!(f, "distort"),
207 Scalers::FitContain => write!(f, "fit_contain"),
208 Scalers::ForcedCover => write!(f, "forced_cover"),
209 }
210 }
211}
212
213#[derive(Serialize)]
241pub struct UeConf<'a> {
242 pub action: Actions,
243 pub path: &'a str,
244 pub identifier: &'a str,
245 pub x: u16,
246 pub y: u16,
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub width: Option<u16>,
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub height: Option<u16>,
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub scaler: Option<Scalers>,
253 #[serde(skip_serializing_if = "Option::is_none")]
254 pub draw: Option<bool>,
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub synchronously_draw: Option<bool>,
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub scaling_position_x: Option<f32>,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub scaling_position_y: Option<f32>,
261}
262
263impl<'a> Default for UeConf<'a> {
264 fn default() -> Self {
265 Self {
266 action: Actions::Add,
267 identifier: "",
268 x: 0,
269 y: 0,
270 path: "",
271 width: None,
272 height: None,
273 scaler: None,
274 draw: None,
275 synchronously_draw: None,
276 scaling_position_x: None,
277 scaling_position_y: None,
278 }
279 }
280}
281
282impl<'a> UeConf<'a> {
283 fn remove_json(identifier: &'a str) -> ResultSerdeJson<String> {
284 let config = Self {
285 action: Actions::Remove,
286 identifier,
287 ..Default::default()
288 };
289 serde_json::to_string(&config)
290 }
291
292 fn add_json(image: &DisplayedImage, rect: Rect) -> ResultSerdeJson<String> {
293 let path = &image.selected_path();
294 let x = rect.x;
295 let y = rect.y.saturating_sub(1);
296 let width = Some(rect.width);
297 let height = Some(rect.height.saturating_sub(1));
298 let scaler = Some(Scalers::FitContain);
299 let config = UeConf {
300 identifier: "fm_tui",
301 path,
302 x,
303 y,
304 width,
305 height,
306 scaler,
307 ..Default::default()
308 };
309
310 serde_json::to_string(&config)
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 #[test]
318 fn enum_to_str() {
319 let add = Actions::Add;
320 let remove = Actions::Remove;
321 assert_eq!(add.to_string(), "add");
322 assert_eq!(format!("{}", remove), "remove");
323 let scaler_1 = Scalers::Contain;
324 let scaler_2 = Scalers::FitContain;
325 assert_eq!(scaler_1.to_string(), "contain");
326 assert_eq!(scaler_2.to_string(), "fit_contain");
327 }
328}