fm/io/
ueberzug.rs

1//! # Ueberzug-rs
2//! [Ueberzug-rs](https://github.com/Adit-Chauhan/Ueberzug-rs) This project provides simple bindings to that [ueberzug](https://github.com/seebye/ueberzug) to draw images in the terminal.
3//!
4//!This code was inspired from the [termusic](https://github.com/tramhao/termusic) to convert their specilized approach to a more general one.
5//!
6//! ## Examples
7//! this example will draw image for 2 seconds, erase the image and wait 1 second before exiting the program.
8//!
9//! This code was copied from the above repository which wasn't maintained anymore
10//! ```
11//! use std::thread::sleep;
12//! use std::time::Duration;
13//! use ueberzug::{UeConf,Scalers};
14//!
15//! let a = ueberzug::Ueberzug::new();
16//! // Draw image
17//! // See UeConf for more details
18//! a.draw(&UeConf {
19//!     identifier: "crab",
20//!     path: "ferris.png",
21//!     x: 10,
22//!     y: 2,
23//!     width: Some(10),
24//!     height: Some(10),
25//!     scaler: Some(Scalers::FitContain),
26//!     ..Default::default()
27//! });
28//! sleep(Duration::from_secs(2));
29//! // Only identifier needed to clear image
30//! a.clear("crab");
31//! sleep(Duration::from_secs(1));
32//! ```
33
34use 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
48/// Check if user has X11 display capabilities.
49/// Call it before trying to spawn ueberzug.
50///
51/// Normal session (terminal emulator from X11 window manager) should have:
52/// - A "DISPLAY" environment variable set,
53/// - no error while running `xset q`, which displays informations about X11 sessions.
54///
55/// If either of these conditions isn't satisfied, the user can't display with ueberzug.
56pub 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
71/// Main Ueberzug Struct
72///
73/// If `self.has_x11` is false, nothing will ever be displayed.
74/// it prevents ueberzug to crash for nothing, trying to open a session.
75pub struct Ueberzug {
76    driver: Child,
77    last_displayed: Option<String>,
78    is_displaying: bool,
79}
80
81impl Default for Ueberzug {
82    /// Creates the Default Ueberzug instance
83    /// One instance can handel multiple images provided they have different identifiers
84    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    /// Draws the Image using ueberzug
95    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    /// Clear the drawn image.
109    /// Only requires the identifier
110    fn clear(&mut self, _: &DisplayedImage) -> Result<()> {
111        if self.is_displaying {
112            self.clear_internal()
113        } else {
114            Ok(())
115        }
116    }
117
118    /// Clear the last image.
119    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    /// true iff the same image was already displayed
134    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/// Action enum for the json value
167#[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/// Scalers that can be applied to the image and are supported by ueberzug
184#[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/// The configuration struct for the image drawing.
214///
215/// *identifier* and *path* are the only required fields and will throw a panic if left empty.
216///
217/// By default *x* and *y* will be set to 0 and all other option will be set to None
218///
219/// ## Example
220/// ```
221/// use ueberzug::UeConf;
222/// // The minimum required for proper config struct.
223/// let conf = UeConf{
224///             identifier:"carb",
225///             path:"ferris.png",
226///             ..Default::default()
227///             };
228///
229/// // More specific option with starting x and y cordinates with width and height
230/// let conf = UeConf{
231///             identifier:"crab",
232///             path:"ferris.png",
233///             x:20,
234///             y:5,
235///             width:Some(30),
236///             height:Some(30),
237///             ..Default::default()
238///             };
239///```
240#[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}