use std::env::var;
use std::fmt;
use std::io::Write;
use std::process::{Child, Command, Stdio};
use anyhow::{Context, Result};
use ratatui::layout::Rect;
use serde::Serialize;
use serde_json::Result as ResultSerdeJson;
use crate::common::UEBERZUG;
use crate::io::ImageDisplayer;
use crate::modes::{DisplayedImage, Quote};
pub fn user_has_x11() -> bool {
if var("DISPLAY").is_err() {
return false;
}
Command::new("xset")
.arg("q")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub struct Ueberzug {
driver: Child,
last_displayed: Option<String>,
is_displaying: bool,
}
impl Default for Ueberzug {
fn default() -> Self {
Self {
driver: Self::spawn_ueberzug().unwrap(),
last_displayed: None,
is_displaying: false,
}
}
}
impl ImageDisplayer for Ueberzug {
fn draw(&mut self, image: &DisplayedImage, rect: Rect) -> Result<()> {
let path = image.selected_path().quote()?;
if self.is_the_same_image(&path) {
Ok(())
} else {
self.clear(image)?;
self.is_displaying = true;
self.last_displayed = Some(path.to_string());
self.run(&UeConf::add_json(image, rect)?)
}
}
fn clear(&mut self, _: &DisplayedImage) -> Result<()> {
if self.is_displaying {
self.clear_internal()
} else {
Ok(())
}
}
fn clear_all(&mut self) -> Result<()> {
self.is_displaying = false;
self.last_displayed = None;
self.driver = Self::spawn_ueberzug()?;
Ok(())
}
}
impl Ueberzug {
fn clear_internal(&mut self) -> Result<()> {
self.is_displaying = false;
self.last_displayed = None;
self.run(&UeConf::remove_json("fm_tui")?)
}
fn is_the_same_image(&mut self, new: &str) -> bool {
let Some(last) = &self.last_displayed else {
return false;
};
last == new
}
fn spawn_ueberzug() -> std::io::Result<Child> {
std::process::Command::new(UEBERZUG)
.arg("layer")
.arg("--silent")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
}
fn run(&mut self, cmd: &str) -> Result<()> {
self.driver
.stdin
.as_mut()
.context("stdin shouldn't be None")?
.write_all(cmd.as_bytes())?;
self.driver
.stdin
.as_mut()
.context("stdin shouldn't be None")?
.write_all(b"\n")?;
Ok(())
}
}
#[derive(Serialize)]
pub enum Actions {
#[serde(rename(serialize = "add"))]
Add,
#[serde(rename(serialize = "remove"))]
Remove,
}
impl fmt::Display for Actions {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Actions::Add => write!(f, "add"),
Actions::Remove => write!(f, "remove"),
}
}
}
#[derive(Clone, Copy, Serialize)]
pub enum Scalers {
#[serde(rename(serialize = "crop"))]
Crop,
#[serde(rename(serialize = "distort"))]
Distort,
#[serde(rename(serialize = "fit_contain"))]
FitContain,
#[serde(rename(serialize = "contain"))]
Contain,
#[serde(rename(serialize = "forced_cover"))]
ForcedCover,
#[serde(rename(serialize = "cover"))]
Cover,
}
impl fmt::Display for Scalers {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Scalers::Contain => write!(f, "contain"),
Scalers::Cover => write!(f, "cover"),
Scalers::Crop => write!(f, "crop"),
Scalers::Distort => write!(f, "distort"),
Scalers::FitContain => write!(f, "fit_contain"),
Scalers::ForcedCover => write!(f, "forced_cover"),
}
}
}
#[derive(Serialize)]
pub struct UeConf<'a> {
pub action: Actions,
pub path: &'a str,
pub identifier: &'a str,
pub x: u16,
pub y: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scaler: Option<Scalers>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draw: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub synchronously_draw: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scaling_position_x: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scaling_position_y: Option<f32>,
}
impl<'a> Default for UeConf<'a> {
fn default() -> Self {
Self {
action: Actions::Add,
identifier: "",
x: 0,
y: 0,
path: "",
width: None,
height: None,
scaler: None,
draw: None,
synchronously_draw: None,
scaling_position_x: None,
scaling_position_y: None,
}
}
}
impl<'a> UeConf<'a> {
fn remove_json(identifier: &'a str) -> ResultSerdeJson<String> {
let config = Self {
action: Actions::Remove,
identifier,
..Default::default()
};
serde_json::to_string(&config)
}
fn add_json(image: &DisplayedImage, rect: Rect) -> ResultSerdeJson<String> {
let path = &image.selected_path();
let x = rect.x;
let y = rect.y.saturating_sub(1);
let width = Some(rect.width);
let height = Some(rect.height.saturating_sub(1));
let scaler = Some(Scalers::FitContain);
let config = UeConf {
identifier: "fm_tui",
path,
x,
y,
width,
height,
scaler,
..Default::default()
};
serde_json::to_string(&config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enum_to_str() {
let add = Actions::Add;
let remove = Actions::Remove;
assert_eq!(add.to_string(), "add");
assert_eq!(format!("{}", remove), "remove");
let scaler_1 = Scalers::Contain;
let scaler_2 = Scalers::FitContain;
assert_eq!(scaler_1.to_string(), "contain");
assert_eq!(scaler_2.to_string(), "fit_contain");
}
}