From 7177317465c92d4c4683563ca48f4ed2cffb7a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Tue, 21 May 2024 02:40:51 +0800 Subject: [PATCH] feat: Chafa integration (#1066) --- Cargo.lock | 1 + yazi-adaptor/Cargo.toml | 1 + yazi-adaptor/src/adaptor.rs | 49 +++++++++------------- yazi-adaptor/src/chafa.rs | 76 ++++++++++++++++++++++++++++++++++ yazi-adaptor/src/image.rs | 15 ++++++- yazi-adaptor/src/iterm2.rs | 20 ++++----- yazi-adaptor/src/kitty.rs | 32 +++++++------- yazi-adaptor/src/kitty_old.rs | 14 +++---- yazi-adaptor/src/lib.rs | 3 ++ yazi-adaptor/src/sixel.rs | 20 ++++----- yazi-adaptor/src/ueberzug.rs | 24 +++++------ yazi-plugin/src/utils/image.rs | 6 +-- 12 files changed, 170 insertions(+), 91 deletions(-) create mode 100644 yazi-adaptor/src/chafa.rs diff --git a/Cargo.lock b/Cargo.lock index 87ea2830..ca66e99d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2693,6 +2693,7 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" name = "yazi-adaptor" version = "0.2.5" dependencies = [ + "ansi-to-tui", "anyhow", "arc-swap", "base64 0.22.1", diff --git a/yazi-adaptor/Cargo.toml b/yazi-adaptor/Cargo.toml index 21e584d7..ca61b09f 100644 --- a/yazi-adaptor/Cargo.toml +++ b/yazi-adaptor/Cargo.toml @@ -13,6 +13,7 @@ yazi-config = { path = "../yazi-config", version = "0.2.5" } yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies +ansi-to-tui = "3.1.0" anyhow = "1.0.86" arc-swap = "1.7.1" base64 = "0.22.1" diff --git a/yazi-adaptor/src/adaptor.rs b/yazi-adaptor/src/adaptor.rs index 0cece50a..954d43dc 100644 --- a/yazi-adaptor/src/adaptor.rs +++ b/yazi-adaptor/src/adaptor.rs @@ -3,10 +3,10 @@ use std::{env, fmt::Display, path::Path, sync::Arc}; use anyhow::Result; use ratatui::layout::Rect; use tracing::warn; -use yazi_shared::{env_exists, term::Term}; +use yazi_shared::env_exists; use super::{Iterm2, Kitty, KittyOld}; -use crate::{ueberzug::Ueberzug, Emulator, Sixel, SHOWN, TMUX}; +use crate::{Chafa, Emulator, Sixel, Ueberzug, SHOWN, TMUX}; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Adaptor { @@ -36,48 +36,39 @@ 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, + Self::X11 | Self::Wayland => Ueberzug::image_show(path, max).await, + Self::Chafa => Chafa::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::KittyOld => KittyOld::image_erase(), - Self::Sixel => Sixel::image_erase(rect), - _ => Ueberzug::image_erase(rect), + Self::Kitty => Kitty::image_erase(area), + Self::Iterm2 => Iterm2::image_erase(area), + Self::KittyOld => KittyOld::image_erase(area), + Self::Sixel => Sixel::image_erase(area), + Self::X11 | Self::Wayland => Ueberzug::image_erase(area), + Self::Chafa => Chafa::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..77b98727 --- /dev/null +++ b/yazi-adaptor/src/chafa.rs @@ -0,0 +1,76 @@ +use std::{io::Write, path::Path, process::Stdio}; + +use ansi_to_tui::IntoText; +use anyhow::{bail, Result}; +use ratatui::layout::Rect; +use tokio::process::Command; +use yazi_shared::term::Term; + +use crate::Adaptor; + +pub(super) struct Chafa; + +impl Chafa { + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + let output = Command::new("chafa") + .args([ + "-f", + "symbols", + "--relative", + "off", + "--polite", + "on", + "--passthrough", + "none", + "--animate", + "off", + "--view-size", + ]) + .arg(format!("{}x{}", max.width, max.height)) + .arg(path) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .kill_on_drop(true) + .output() + .await?; + + if !output.status.success() { + bail!("chafa failed with status: {}", output.status); + } else if output.stdout.is_empty() { + bail!("chafa returned no output"); + } + + let lines: Vec<_> = output.stdout.split(|&b| b == b'\n').collect(); + let Ok(Some(first)) = lines[0].into_text().map(|mut t| t.lines.pop()) else { + bail!("failed to parse chafa output"); + }; + + let area = Rect { + x: max.x, + y: max.y, + width: first.spans.into_iter().map(|s| s.content.chars().count() as u16).sum(), + height: lines.len() as u16, + }; + + Adaptor::shown_store(area); + Term::move_lock((max.x, max.y), |stderr| { + for (i, line) in lines.into_iter().enumerate() { + stderr.write_all(line)?; + Term::move_to(stderr, max.x, max.y + i as u16 + 1)?; + } + Ok(area) + }) + } + + pub(super) fn image_erase(area: Rect) -> Result<()> { + let s = " ".repeat(area.width as usize); + Term::move_lock((0, 0), |stderr| { + for y in area.top()..area.bottom() { + Term::move_to(stderr, area.x, y)?; + write!(stderr, "{s}")?; + } + Ok(()) + }) + } +} 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..46b6814d 100644 --- a/yazi-adaptor/src/kitty.rs +++ b/yazi-adaptor/src/kitty.rs @@ -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..1e2250e5 100644 --- a/yazi-adaptor/src/kitty_old.rs +++ b/yazi-adaptor/src/kitty_old.rs @@ -13,21 +13,21 @@ 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) }) } #[inline] - pub(super) fn image_erase() -> Result<()> { + pub(super) fn image_erase(_: Rect) -> Result<()> { let mut stderr = LineWriter::new(stderr()); write!(stderr, "{}_Gq=1,a=d,d=A{}\\{}", START, ESCAPE, CLOSE)?; stderr.flush()?; diff --git a/yazi-adaptor/src/lib.rs b/yazi-adaptor/src/lib.rs index bf534082..65675fc7 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; @@ -10,11 +11,13 @@ mod sixel; mod ueberzug; pub use adaptor::*; +use chafa::*; pub use emulator::*; use iterm2::*; use kitty::*; use kitty_old::*; use sixel::*; +use ueberzug::*; use yazi_shared::{env_exists, RoCell}; pub use crate::image::*; 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..2618e80c 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(), area)))?; - 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<()> { @@ -77,6 +72,7 @@ impl Ueberzug { .env("SPDLOG_LEVEL", if cfg!(debug_assertions) { "debug" } else { "" }) .kill_on_drop(true) .stdin(Stdio::piped()) + .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn(); diff --git a/yazi-plugin/src/utils/image.rs b/yazi-plugin/src/utils/image.rs index 9a13e684..acb72bf4 100644 --- a/yazi-plugin/src/utils/image.rs +++ b/yazi-plugin/src/utils/image.rs @@ -2,15 +2,15 @@ use mlua::{IntoLuaMulti, Lua, Table, Value}; use yazi_adaptor::{Image, ADAPTOR}; use super::Utils; -use crate::{elements::RectRef, url::UrlRef}; +use crate::{bindings::Cast, elements::{Rect, RectRef}, url::UrlRef}; impl Utils { pub(super) fn image(lua: &Lua, ya: &Table) -> mlua::Result<()> { ya.raw_set( "image_show", lua.create_async_function(|lua, (url, rect): (UrlRef, RectRef)| async move { - if let Ok(size) = ADAPTOR.image_show(&url, *rect).await { - size.into_lua_multi(lua) + if let Ok(area) = ADAPTOR.image_show(&url, *rect).await { + Rect::cast(lua, area)?.into_lua_multi(lua) } else { Value::Nil.into_lua_multi(lua) }