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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
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;
use anyhow::Context;
use anyhow::{Result, bail};
use termusiclib::xywh::Xywh;
#[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 {
info!("Potentially using ueberzug");
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<()> {
let Some(ueberzug) = self.try_wait_spawn(["layer", "--silent"])? 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<()> {
// debug!("ueberzug forced sixel");
let Some(ueberzug) = self.try_wait_spawn(
["layer", "--silent"],
// ["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::inherit()) // ueberzug may need this for chafa output
.stderr(Stdio::piped());
match cmd.spawn() {
Ok(child) => {
self.ueberzug = UeInstanceState::Child(child);
Ok(self.ueberzug.unwrap_child_mut())
}
Err(err) => {
// ueberzug is not installed or available via the command above
if err.kind() == std::io::ErrorKind::NotFound {
self.ueberzug = UeInstanceState::Error;
}
bail!(err)
}
}
}
/// Map a potential [`std::io::Error`] (kind [`NotFound`](std::io::ErrorKind::NotFound)) to `Ok(None)` to ignore the error.
#[inline]
fn map_notfound_ok<T>(err: anyhow::Error) -> Result<Option<T>> {
if err
.downcast_ref::<std::io::Error>()
.is_some_and(|v| v.kind() == std::io::ErrorKind::NotFound)
{
warn!("ueberzug is not installed; not displaying error");
Ok(None)
} else {
Err(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 => match self
.spawn_cmd(args)
.map_or_else(Self::map_notfound_ok, |v| Ok(Some(v)))?
{
Some(child) => child,
None => return Ok(None),
},
UeInstanceState::Child(ref mut v) => v,
UeInstanceState::Error => {
trace!("Not re-trying ueberzug, because it has a permanent error!");
return Ok(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()))
}
}
/// Map a given error to include extra context
#[inline]
fn map_err(err: anyhow::Error) -> anyhow::Error {
err.context("Failed to run Ueberzug")
}