From d51962ce837419784644f45602d52d50e6b19dca Mon Sep 17 00:00:00 2001 From: sxyazi Date: Tue, 21 May 2024 00:32:13 +0800 Subject: [PATCH] feat: Chafa integration --- yazi-adaptor/src/adaptor.rs | 41 +++++++++-------------- yazi-adaptor/src/chafa.rs | 63 +++++++++++++++++++++++++++++++++++ yazi-adaptor/src/image.rs | 15 +++++++-- yazi-adaptor/src/iterm2.rs | 20 +++++------ yazi-adaptor/src/kitty.rs | 34 +++++++++---------- yazi-adaptor/src/kitty_old.rs | 12 +++---- yazi-adaptor/src/lib.rs | 1 + yazi-adaptor/src/sixel.rs | 20 +++++------ yazi-adaptor/src/ueberzug.rs | 23 +++++-------- 9 files changed, 144 insertions(+), 85 deletions(-) create mode 100644 yazi-adaptor/src/chafa.rs diff --git a/yazi-adaptor/src/adaptor.rs b/yazi-adaptor/src/adaptor.rs index 0cece50a..b85b5afc 100644 --- a/yazi-adaptor/src/adaptor.rs +++ b/yazi-adaptor/src/adaptor.rs @@ -36,48 +36,37 @@ impl Display for Adaptor { } impl Adaptor { - pub async fn image_show(self, path: &Path, rect: Rect) -> Result<(u32, u32)> { + pub async fn image_show(self, path: &Path, max: Rect) -> Result { match self { - Self::Kitty => Kitty::image_show(path, rect).await, - Self::KittyOld => KittyOld::image_show(path, rect).await, - Self::Iterm2 => Iterm2::image_show(path, rect).await, - Self::Sixel => Sixel::image_show(path, rect).await, - _ => Ueberzug::image_show(path, rect).await, + Self::Kitty => Kitty::image_show(path, max).await, + Self::KittyOld => KittyOld::image_show(path, max).await, + Self::Iterm2 => Iterm2::image_show(path, max).await, + Self::Sixel => Sixel::image_show(path, max).await, + _ => Ueberzug::image_show(path, max).await, } } pub fn image_hide(self) -> Result<()> { - if let Some(rect) = SHOWN.swap(None) { self.image_erase(*rect) } else { Ok(()) } + if let Some(area) = SHOWN.swap(None) { self.image_erase(*area) } else { Ok(()) } } - pub fn image_erase(self, rect: Rect) -> Result<()> { + pub fn image_erase(self, area: Rect) -> Result<()> { match self { - Self::Kitty => Kitty::image_erase(rect), - Self::Iterm2 => Iterm2::image_erase(rect), + Self::Kitty => Kitty::image_erase(area), + Self::Iterm2 => Iterm2::image_erase(area), Self::KittyOld => KittyOld::image_erase(), - Self::Sixel => Sixel::image_erase(rect), - _ => Ueberzug::image_erase(rect), + Self::Sixel => Sixel::image_erase(area), + _ => Ueberzug::image_erase(area), } } #[inline] pub fn shown_load(self) -> Option { SHOWN.load_full().map(|r| *r) } - pub(super) fn start(self) { Ueberzug::start(self); } - #[inline] - pub(super) fn shown_store(rect: Rect, size: (u32, u32)) { - SHOWN.store(Some(Arc::new( - Term::ratio() - .map(|(r1, r2)| Rect { - x: rect.x, - y: rect.y, - width: (size.0 as f64 / r1).ceil() as u16, - height: (size.1 as f64 / r2).ceil() as u16, - }) - .unwrap_or(rect), - ))); - } + pub(super) fn shown_store(area: Rect) { SHOWN.store(Some(Arc::new(area))); } + + pub(super) fn start(self) { Ueberzug::start(self); } #[inline] pub(super) fn needs_ueberzug(self) -> bool { diff --git a/yazi-adaptor/src/chafa.rs b/yazi-adaptor/src/chafa.rs new file mode 100644 index 00000000..06086335 --- /dev/null +++ b/yazi-adaptor/src/chafa.rs @@ -0,0 +1,63 @@ +use std::{path::Path, process::Stdio}; + +use anyhow::{bail, Result}; +use imagesize::ImageSize; +use ratatui::layout::Rect; +use tokio::process::Command; + +use crate::Image; + +pub(super) struct Chafa; + +impl Chafa { + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { + let p = path.to_owned(); + let ImageSize { width: w, height: h } = + tokio::task::spawn_blocking(move || imagesize::size(p)).await??; + + let output = Command::new("chafa") + .args([ + "-f", + "symbols", + "--relative", + "off", + "--polite", + "on", + "--passthrough", + "none", + "--animate", + "off", + "--view-size", + ]) + .arg(format!("{}x{}", rect.width, rect.height)) + .arg(path) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .stdout(Stdio::piped()) + .kill_on_drop(true) + .output() + .await?; + + if !output.status.success() { + bail!("chafa failed with status: {}", output.status); + } + + // output.stdout + + let (max_w, max_h) = Image::max_pixel(rect); + if w <= max_w as usize && h <= max_h as usize { + return Ok((w as u32, h as u32)); + } + + let ratio = f64::min(max_w as f64 / w as f64, max_h as f64 / h as f64); + Ok(((w as f64 * ratio).round() as u32, (h as f64 * ratio).round() as u32)) + } + + pub(super) fn image_erase(_: Rect) -> Result<()> { + if let Some(tx) = &*DEMON { + Ok(tx.send(None)?) + } else { + bail!("uninitialized ueberzugpp"); + } + } +} diff --git a/yazi-adaptor/src/image.rs b/yazi-adaptor/src/image.rs index 3fbb3b9f..d21575df 100644 --- a/yazi-adaptor/src/image.rs +++ b/yazi-adaptor/src/image.rs @@ -57,7 +57,7 @@ impl Image { }) .await??; - let (mut w, mut h) = Self::max_size(rect); + let (mut w, mut h) = Self::max_pixel(rect); if (5..=8).contains(&orientation) { (w, h) = (h, w); } @@ -76,7 +76,7 @@ impl Image { .await? } - pub(super) fn max_size(rect: Rect) -> (u32, u32) { + pub(super) fn max_pixel(rect: Rect) -> (u32, u32) { Term::ratio() .map(|(r1, r2)| { let (w, h) = ((rect.width as f64 * r1) as u32, (rect.height as f64 * r2) as u32); @@ -85,6 +85,17 @@ impl Image { .unwrap_or((PREVIEW.max_width, PREVIEW.max_height)) } + pub(super) fn pixel_area(size: (u32, u32), rect: Rect) -> Rect { + Term::ratio() + .map(|(r1, r2)| Rect { + x: rect.x, + y: rect.y, + width: (size.0 as f64 / r1).ceil() as u16, + height: (size.1 as f64 / r2).ceil() as u16, + }) + .unwrap_or(rect) + } + #[inline] fn filter() -> FilterType { match PREVIEW.image_filter.as_str() { diff --git a/yazi-adaptor/src/iterm2.rs b/yazi-adaptor/src/iterm2.rs index 5efcfce6..4c0538f3 100644 --- a/yazi-adaptor/src/iterm2.rs +++ b/yazi-adaptor/src/iterm2.rs @@ -12,24 +12,24 @@ use crate::{adaptor::Adaptor, CLOSE, START}; pub(super) struct Iterm2; impl Iterm2 { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { - let img = Image::downscale(path, rect).await?; - let size = (img.width(), img.height()); + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + let img = Image::downscale(path, max).await?; + let area = Image::pixel_area((img.width(), img.height()), max); let b = Self::encode(img).await?; Adaptor::Iterm2.image_hide()?; - Adaptor::shown_store(rect, size); - Term::move_lock((rect.x, rect.y), |stderr| { + Adaptor::shown_store(area); + Term::move_lock((max.x, max.y), |stderr| { stderr.write_all(&b)?; - Ok(size) + Ok(area) }) } - pub(super) fn image_erase(rect: Rect) -> Result<()> { - let s = " ".repeat(rect.width as usize); + pub(super) fn image_erase(area: Rect) -> Result<()> { + let s = " ".repeat(area.width as usize); Term::move_lock((0, 0), |stderr| { - for y in rect.top()..rect.bottom() { - Term::move_to(stderr, rect.x, y)?; + for y in area.top()..area.bottom() { + Term::move_to(stderr, area.x, y)?; write!(stderr, "{s}")?; } Ok(()) diff --git a/yazi-adaptor/src/kitty.rs b/yazi-adaptor/src/kitty.rs index cdc46cb4..cd219343 100644 --- a/yazi-adaptor/src/kitty.rs +++ b/yazi-adaptor/src/kitty.rs @@ -3,7 +3,7 @@ use std::{io::Write, path::Path}; use anyhow::Result; use base64::{engine::general_purpose, Engine}; -use image::DynamicImage; +use image::{DynamicImage, GenericImageView}; use ratatui::layout::Rect; use yazi_shared::term::Term; @@ -313,27 +313,27 @@ static DIACRITICS: [char; 297] = [ pub(super) struct Kitty; impl Kitty { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { - let img = Image::downscale(path, rect).await?; - let size = (img.width(), img.height()); + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + let img = Image::downscale(path, max).await?; + let area = Image::pixel_area((img.width(), img.height()), max); let b1 = Self::encode(img).await?; - let b2 = Self::place(&rect)?; + let b2 = Self::place(&area)?; Adaptor::Kitty.image_hide()?; - Adaptor::shown_store(rect, size); - Term::move_lock((rect.x, rect.y), |stderr| { + Adaptor::shown_store(area); + Term::move_lock((area.x, area.y), |stderr| { stderr.write_all(&b1)?; stderr.write_all(&b2)?; - Ok(size) + Ok(area) }) } - pub(super) fn image_erase(rect: Rect) -> Result<()> { - let s = " ".repeat(rect.width as usize); + pub(super) fn image_erase(area: Rect) -> Result<()> { + let s = " ".repeat(area.width as usize); Term::move_lock((0, 0), |stderr| { - for y in rect.top()..rect.bottom() { - Term::move_to(stderr, rect.x, y)?; + for y in area.top()..area.bottom() { + Term::move_to(stderr, area.x, y)?; write!(stderr, "{s}")?; } @@ -388,11 +388,11 @@ impl Kitty { .await? } - fn place(rect: &Rect) -> Result> { - let mut buf = Vec::with_capacity(rect.width as usize * rect.height as usize * 3 + 50); - for y in 0..rect.height { - write!(buf, "\x1b[{};{}H\x1b[38;5;1m", rect.y + y + 1, rect.x + 1)?; - for x in 0..rect.width { + fn place(area: &Rect) -> Result> { + let mut buf = Vec::with_capacity(area.width as usize * area.height as usize * 3 + 50); + for y in 0..area.height { + write!(buf, "\x1b[{};{}H\x1b[38;5;1m", area.y + y + 1, area.x + 1)?; + for x in 0..area.width { write!(buf, "\u{10EEEE}")?; write!(buf, "{}", *DIACRITICS.get(y as usize).unwrap_or(&DIACRITICS[0]))?; write!(buf, "{}", *DIACRITICS.get(x as usize).unwrap_or(&DIACRITICS[0]))?; diff --git a/yazi-adaptor/src/kitty_old.rs b/yazi-adaptor/src/kitty_old.rs index cf0a2df9..03e878b2 100644 --- a/yazi-adaptor/src/kitty_old.rs +++ b/yazi-adaptor/src/kitty_old.rs @@ -13,16 +13,16 @@ use crate::{adaptor::Adaptor, CLOSE, ESCAPE, START}; pub(super) struct KittyOld; impl KittyOld { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { - let img = Image::downscale(path, rect).await?; - let size = (img.width(), img.height()); + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + let img = Image::downscale(path, max).await?; + let area = Image::pixel_area((img.width(), img.height()), max); let b = Self::encode(img).await?; Adaptor::KittyOld.image_hide()?; - Adaptor::shown_store(rect, size); - Term::move_lock((rect.x, rect.y), |stderr| { + Adaptor::shown_store(area); + Term::move_lock((area.x, area.y), |stderr| { stderr.write_all(&b)?; - Ok(size) + Ok(area) }) } diff --git a/yazi-adaptor/src/lib.rs b/yazi-adaptor/src/lib.rs index bf534082..00cc11f1 100644 --- a/yazi-adaptor/src/lib.rs +++ b/yazi-adaptor/src/lib.rs @@ -1,6 +1,7 @@ #![allow(clippy::unit_arg)] mod adaptor; +mod chafa; mod emulator; mod image; mod iterm2; diff --git a/yazi-adaptor/src/sixel.rs b/yazi-adaptor/src/sixel.rs index eb4b22ab..91f6f1fa 100644 --- a/yazi-adaptor/src/sixel.rs +++ b/yazi-adaptor/src/sixel.rs @@ -12,24 +12,24 @@ use crate::{adaptor::Adaptor, Image, CLOSE, ESCAPE, START}; pub(super) struct Sixel; impl Sixel { - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { - let img = Image::downscale(path, rect).await?; - let size = (img.width(), img.height()); + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + let img = Image::downscale(path, max).await?; + let area = Image::pixel_area((img.width(), img.height()), max); let b = Self::encode(img).await?; Adaptor::Sixel.image_hide()?; - Adaptor::shown_store(rect, size); - Term::move_lock((rect.x, rect.y), |stderr| { + Adaptor::shown_store(area); + Term::move_lock((area.x, area.y), |stderr| { stderr.write_all(&b)?; - Ok(size) + Ok(area) }) } - pub(super) fn image_erase(rect: Rect) -> Result<()> { - let s = " ".repeat(rect.width as usize); + pub(super) fn image_erase(area: Rect) -> Result<()> { + let s = " ".repeat(area.width as usize); Term::move_lock((0, 0), |stderr| { - for y in rect.top()..rect.bottom() { - Term::move_to(stderr, rect.x, y)?; + for y in area.top()..area.bottom() { + Term::move_to(stderr, area.x, y)?; write!(stderr, "{s}")?; } Ok(()) diff --git a/yazi-adaptor/src/ueberzug.rs b/yazi-adaptor/src/ueberzug.rs index 1e3a0106..06c2fc6b 100644 --- a/yazi-adaptor/src/ueberzug.rs +++ b/yazi-adaptor/src/ueberzug.rs @@ -41,25 +41,20 @@ impl Ueberzug { DEMON.init(Some(tx)) } - pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<(u32, u32)> { - if let Some(tx) = &*DEMON { - tx.send(Some((path.to_path_buf(), rect)))?; - Adaptor::shown_store(rect, (0, 0)); - } else { + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + let Some(tx) = &*DEMON else { bail!("uninitialized ueberzugpp"); - } + }; - let path = path.to_owned(); + let p = path.to_owned(); let ImageSize { width: w, height: h } = - tokio::task::spawn_blocking(move || imagesize::size(path)).await??; + tokio::task::spawn_blocking(move || imagesize::size(p)).await??; - let (max_w, max_h) = Image::max_size(rect); - if w <= max_w as usize && h <= max_h as usize { - return Ok((w as u32, h as u32)); - } + let area = Image::pixel_area((w as u32, h as u32), max); + tx.send(Some((path.to_owned(), max)))?; - let ratio = f64::min(max_w as f64 / w as f64, max_h as f64 / h as f64); - Ok(((w as f64 * ratio).round() as u32, (h as f64 * ratio).round() as u32)) + Adaptor::shown_store(area); + Ok(area) } pub(super) fn image_erase(_: Rect) -> Result<()> {