termusiclib/ueberzug.rs
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 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
use crate::xywh::Xywh;
use anyhow::Context;
use anyhow::{bail, Result};
use std::ffi::OsStr;
use std::io::Read as _;
use std::io::Write;
use std::process::Child;
use std::process::Command;
use std::process::Stdio;
#[derive(Debug)]
pub enum UeInstanceState {
New,
Child(Child),
/// Permanent Error
Error,
}
impl PartialEq for UeInstanceState {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Child(_), Self::Child(_)) => true,
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
}
impl UeInstanceState {
/// unwrap value in [`UeInstanceState::Child`], panicing if not that variant
fn unwrap_child_mut(&mut self) -> &mut Child {
if let Self::Child(v) = self {
return v;
}
unreachable!()
}
}
/// Run `ueberzug` commands
///
/// If there is a permanent error (like `ueberzug` not being installed), will silently ignore all commands after initial error
#[derive(Debug)]
pub struct UeInstance {
ueberzug: UeInstanceState,
}
impl Default for UeInstance {
fn default() -> Self {
Self {
ueberzug: UeInstanceState::New,
}
}
}
impl UeInstance {
pub fn draw_cover_ueberzug(
&mut self,
url: &str,
draw_xywh: &Xywh,
use_sixel: bool,
) -> Result<()> {
if draw_xywh.width <= 1 || draw_xywh.height <= 1 {
return Ok(());
}
// Ueberzug takes an area given in chars and fits the image to
// that area (from the top left).
// draw_offset.y += (draw_size.y - size.y) - (draw_size.y - size.y) / 2;
let cmd = format!("{{\"action\":\"add\",\"scaler\":\"forced_cover\",\"identifier\":\"cover\",\"x\":{},\"y\":{},\"width\":{},\"height\":{},\"path\":\"{}\"}}\n",
// let cmd = format!("{{\"action\":\"add\",\"scaler\":\"fit_contain\",\"identifier\":\"cover\",\"x\":{},\"y\":{},\"width\":{},\"height\":{},\"path\":\"{}\"}}\n",
// TODO: right now the y position of ueberzug is not consistent, and could be a 0.5 difference
// draw_xywh.x, draw_xywh.y-1,
draw_xywh.x, draw_xywh.y,//-1 + (draw_xywh.width-draw_xywh.height) % 2,
draw_xywh.width,draw_xywh.height/2,//+ (draw_xywh.width-draw_xywh.height)%2,
url,
);
// debug!(
// "draw_xywh.x = {}, draw_xywh.y = {}, draw_wyxh.width = {}, draw_wyxh.height = {}",
// draw_xywh.x, draw_xywh.y, draw_xywh.width, draw_xywh.height,
// );
if use_sixel {
self.run_ueberzug_cmd_sixel(&cmd).map_err(map_err)?;
} else {
self.run_ueberzug_cmd(&cmd).map_err(map_err)?;
};
Ok(())
}
pub fn clear_cover_ueberzug(&mut self) -> Result<()> {
let cmd = "{\"action\": \"remove\", \"identifier\": \"cover\"}\n";
self.run_ueberzug_cmd(cmd)
.map_err(map_err)
.context("clear_cover")?;
Ok(())
}
fn run_ueberzug_cmd(&mut self, cmd: &str) -> Result<()> {
// error!("using x11 output for ueberzugpp");
let Some(ueberzug) = self.try_wait_spawn(["layer"])? else {
return Ok(());
};
let stdin = ueberzug.stdin.as_mut().unwrap();
stdin
.write_all(cmd.as_bytes())
.context("ueberzug command writing")?;
Ok(())
}
fn run_ueberzug_cmd_sixel(&mut self, cmd: &str) -> Result<()> {
// error!("using sixel output for ueberzugpp");
let Some(ueberzug) = self.try_wait_spawn(
["layer"],
// ["layer", "--silent", "--no-cache", "--output", "sixel"]
// ["layer", "--sixel"]
// ["--sixel"]
)?
else {
return Ok(());
};
let stdin = ueberzug.stdin.as_mut().unwrap();
stdin
.write_all(cmd.as_bytes())
.context("ueberzug command writing")?;
Ok(())
}
/// Spawn the given `cmd`, and set `self.ueberzug` and return a reference to the child for direct use
///
/// On fail, also set `set.ueberzug` to [`UeInstanceState::Error`]
fn spawn_cmd<I, S>(&mut self, args: I) -> Result<&mut Child>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut cmd = Command::new("ueberzug");
cmd.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::null()) // ueberzug does not output to stdout
.stderr(Stdio::piped());
match cmd.spawn() {
Ok(child) => {
self.ueberzug = UeInstanceState::Child(child);
return Ok(self.ueberzug.unwrap_child_mut());
}
Err(err) => {
if err.kind() == std::io::ErrorKind::NotFound {
self.ueberzug = UeInstanceState::Error;
}
bail!(err)
}
}
}
/// If ueberzug instance does not exist, create it. Otherwise take the existing one
///
/// Do a [`Child::try_wait`] on the existing instance and return a error if the instance has exited
fn try_wait_spawn<I, S>(&mut self, args: I) -> Result<Option<&mut Child>>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let child = match self.ueberzug {
UeInstanceState::New => self.spawn_cmd(args)?,
UeInstanceState::Child(ref mut v) => v,
UeInstanceState::Error => return on_error().map(|()| None),
};
if let Some(exit_status) = child.try_wait()? {
let mut stderr_buf = String::new();
child
.stderr
.as_mut()
.map(|v| v.read_to_string(&mut stderr_buf));
// using a permanent-Error because it is likely the error will happen again on restart (like being on wayland instead of x11)
self.ueberzug = UeInstanceState::Error;
if stderr_buf.is_empty() {
stderr_buf.push_str("<empty>");
}
// special handling for unix as that only contains the ".signal" extension, which is important there
#[cfg(not(target_family = "unix"))]
{
bail!(
"ueberzug command closed unexpectedly, (code {:?}), stderr:\n{}",
exit_status.code(),
stderr_buf
);
}
#[cfg(target_family = "unix")]
{
use std::os::unix::process::ExitStatusExt as _;
bail!(
"ueberzug command closed unexpectedly, (code {:?}, signal {:?}), stderr:\n{}",
exit_status.code(),
exit_status.signal(),
stderr_buf
);
}
}
// out of some reason local variable "child" cannot be returned here because it is modified in the "try_wait" branch
// even though that branch never reaches here
Ok(Some(self.ueberzug.unwrap_child_mut()))
}
}
/// Small helper to always print a message and return a consistent return
#[inline]
#[allow(clippy::unnecessary_wraps)]
fn on_error() -> Result<()> {
trace!("Not re-trying ueberzug, because it has a permanent error!");
Ok(())
}
/// Map a given error to include extra context
#[inline]
#[allow(clippy::needless_pass_by_value)]
fn map_err(err: anyhow::Error) -> anyhow::Error {
err.context("Failed to run Ueberzug")
}