termusiclib/
ueberzug.rs

1use crate::xywh::Xywh;
2use anyhow::Context;
3use anyhow::{Result, bail};
4use std::ffi::OsStr;
5use std::io::Read as _;
6use std::io::Write;
7use std::process::Child;
8use std::process::Command;
9use std::process::Stdio;
10
11#[derive(Debug)]
12pub enum UeInstanceState {
13    New,
14    Child(Child),
15    /// Permanent Error
16    Error,
17}
18
19impl PartialEq for UeInstanceState {
20    fn eq(&self, other: &Self) -> bool {
21        match (self, other) {
22            (Self::Child(_), Self::Child(_)) => true,
23            _ => core::mem::discriminant(self) == core::mem::discriminant(other),
24        }
25    }
26}
27
28impl UeInstanceState {
29    /// unwrap value in [`UeInstanceState::Child`], panicing if not that variant
30    fn unwrap_child_mut(&mut self) -> &mut Child {
31        if let Self::Child(v) = self {
32            return v;
33        }
34        unreachable!()
35    }
36}
37
38/// Run `ueberzug` commands
39///
40/// If there is a permanent error (like `ueberzug` not being installed), will silently ignore all commands after initial error
41#[derive(Debug)]
42pub struct UeInstance {
43    ueberzug: UeInstanceState,
44}
45
46impl Default for UeInstance {
47    fn default() -> Self {
48        info!("Potentially using ueberzug");
49
50        Self {
51            ueberzug: UeInstanceState::New,
52        }
53    }
54}
55
56impl UeInstance {
57    pub fn draw_cover_ueberzug(
58        &mut self,
59        url: &str,
60        draw_xywh: &Xywh,
61        use_sixel: bool,
62    ) -> Result<()> {
63        if draw_xywh.width <= 1 || draw_xywh.height <= 1 {
64            return Ok(());
65        }
66
67        // Ueberzug takes an area given in chars and fits the image to
68        // that area (from the top left).
69        //   draw_offset.y += (draw_size.y - size.y) - (draw_size.y - size.y) / 2;
70        let cmd = format!(
71            "{{\"action\":\"add\",\"scaler\":\"forced_cover\",\"identifier\":\"cover\",\"x\":{},\"y\":{},\"width\":{},\"height\":{},\"path\":\"{}\"}}\n",
72            // let cmd = format!("{{\"action\":\"add\",\"scaler\":\"fit_contain\",\"identifier\":\"cover\",\"x\":{},\"y\":{},\"width\":{},\"height\":{},\"path\":\"{}\"}}\n",
73            // TODO: right now the y position of ueberzug is not consistent, and could be a 0.5 difference
74            // draw_xywh.x, draw_xywh.y-1,
75            draw_xywh.x,
76            draw_xywh.y, //-1 + (draw_xywh.width-draw_xywh.height) % 2,
77            draw_xywh.width,
78            draw_xywh.height / 2, //+ (draw_xywh.width-draw_xywh.height)%2,
79            url,
80        );
81
82        // debug!(
83        //     "draw_xywh.x = {}, draw_xywh.y = {}, draw_wyxh.width = {}, draw_wyxh.height = {}",
84        //     draw_xywh.x, draw_xywh.y, draw_xywh.width, draw_xywh.height,
85        // );
86        if use_sixel {
87            self.run_ueberzug_cmd_sixel(&cmd).map_err(map_err)?;
88        } else {
89            self.run_ueberzug_cmd(&cmd).map_err(map_err)?;
90        }
91
92        Ok(())
93    }
94
95    pub fn clear_cover_ueberzug(&mut self) -> Result<()> {
96        let cmd = "{\"action\": \"remove\", \"identifier\": \"cover\"}\n";
97        self.run_ueberzug_cmd(cmd)
98            .map_err(map_err)
99            .context("clear_cover")?;
100        Ok(())
101    }
102
103    fn run_ueberzug_cmd(&mut self, cmd: &str) -> Result<()> {
104        let Some(ueberzug) = self.try_wait_spawn(["layer", "--silent"])? else {
105            return Ok(());
106        };
107
108        let stdin = ueberzug.stdin.as_mut().unwrap();
109        stdin
110            .write_all(cmd.as_bytes())
111            .context("ueberzug command writing")?;
112
113        Ok(())
114    }
115
116    fn run_ueberzug_cmd_sixel(&mut self, cmd: &str) -> Result<()> {
117        // debug!("ueberzug forced sixel");
118
119        let Some(ueberzug) = self.try_wait_spawn(
120            ["layer", "--silent"],
121            // ["layer", "--silent", "--no-cache", "--output", "sixel"]
122            // ["layer", "--sixel"]
123            // ["--sixel"]
124        )?
125        else {
126            return Ok(());
127        };
128
129        let stdin = ueberzug.stdin.as_mut().unwrap();
130        stdin
131            .write_all(cmd.as_bytes())
132            .context("ueberzug command writing")?;
133
134        Ok(())
135    }
136
137    /// Spawn the given `cmd`, and set `self.ueberzug` and return a reference to the child for direct use
138    ///
139    /// On fail, also set `set.ueberzug` to [`UeInstanceState::Error`]
140    fn spawn_cmd<I, S>(&mut self, args: I) -> Result<&mut Child>
141    where
142        I: IntoIterator<Item = S>,
143        S: AsRef<OsStr>,
144    {
145        let mut cmd = Command::new("ueberzug");
146        cmd.args(args)
147            .stdin(Stdio::piped())
148            .stdout(Stdio::inherit()) // ueberzug may need this for chafa output
149            .stderr(Stdio::piped());
150
151        match cmd.spawn() {
152            Ok(child) => {
153                self.ueberzug = UeInstanceState::Child(child);
154                Ok(self.ueberzug.unwrap_child_mut())
155            }
156            Err(err) => {
157                if err.kind() == std::io::ErrorKind::NotFound {
158                    self.ueberzug = UeInstanceState::Error;
159                }
160                bail!(err)
161            }
162        }
163    }
164
165    /// If ueberzug instance does not exist, create it. Otherwise take the existing one
166    ///
167    /// Do a [`Child::try_wait`] on the existing instance and return a error if the instance has exited
168    fn try_wait_spawn<I, S>(&mut self, args: I) -> Result<Option<&mut Child>>
169    where
170        I: IntoIterator<Item = S>,
171        S: AsRef<OsStr>,
172    {
173        let child = match self.ueberzug {
174            UeInstanceState::New => self.spawn_cmd(args)?,
175            UeInstanceState::Child(ref mut v) => v,
176            UeInstanceState::Error => {
177                trace!("Not re-trying ueberzug, because it has a permanent error!");
178
179                return Ok(None);
180            }
181        };
182
183        if let Some(exit_status) = child.try_wait()? {
184            let mut stderr_buf = String::new();
185            child
186                .stderr
187                .as_mut()
188                .map(|v| v.read_to_string(&mut stderr_buf));
189
190            // using a permanent-Error because it is likely the error will happen again on restart (like being on wayland instead of x11)
191            self.ueberzug = UeInstanceState::Error;
192
193            if stderr_buf.is_empty() {
194                stderr_buf.push_str("<empty>");
195            }
196
197            // special handling for unix as that only contains the ".signal" extension, which is important there
198            #[cfg(not(target_family = "unix"))]
199            {
200                bail!(
201                    "ueberzug command closed unexpectedly, (code {:?}), stderr:\n{}",
202                    exit_status.code(),
203                    stderr_buf
204                );
205            }
206            #[cfg(target_family = "unix")]
207            {
208                use std::os::unix::process::ExitStatusExt as _;
209                bail!(
210                    "ueberzug command closed unexpectedly, (code {:?}, signal {:?}), stderr:\n{}",
211                    exit_status.code(),
212                    exit_status.signal(),
213                    stderr_buf
214                );
215            }
216        }
217
218        // out of some reason local variable "child" cannot be returned here because it is modified in the "try_wait" branch
219        // even though that branch never reaches here
220        Ok(Some(self.ueberzug.unwrap_child_mut()))
221    }
222}
223
224/// Map a given error to include extra context
225#[inline]
226fn map_err(err: anyhow::Error) -> anyhow::Error {
227    err.context("Failed to run Ueberzug")
228}