From 1b97e513f1b1f5cb439baaf694dbcc8b9e92db04 Mon Sep 17 00:00:00 2001 From: gaesa <71256557+gaesa@users.noreply.github.com> Date: Mon, 14 Oct 2024 23:14:31 +0800 Subject: [PATCH] feat: support customizable alignment for image display This is an attempt to provide a possible solution to #1141. - Adds configuration options in `yazi.toml` for setting preferred image alignment, which plugin developers can optionally respect. - Introduces the `image_area` helper to streamline custom image placement logic for plugins. - Refactors image display logic by extracting the `image_area` function from various `image_show` implementations and exposing it in `adapter.rs`, facilitating Lua integration. - Updates plugin defaults to use `image_show_aligned` for aligned image display. - Ensures backward compatibility for existing plugins. It appears to work well overall, though some aspects remain unclear: 1. Does the current use of `serde` align with usual practices? 2. Are the new Lua API functions consistent with current design philosophy? 3. Do the helper functions follow the preferred style? --- yazi-adapter/src/adapter.rs | 16 +++++++++++++- yazi-adapter/src/chafa.rs | 32 +++++++++++++++++++-------- yazi-adapter/src/iip.rs | 3 +-- yazi-adapter/src/image.rs | 27 ++++++++++++++++++++++ yazi-adapter/src/kgp.rs | 3 +-- yazi-adapter/src/kgp_old.rs | 3 +-- yazi-adapter/src/sixel.rs | 3 +-- yazi-adapter/src/ueberzug.rs | 30 +++++++++++++++---------- yazi-config/preset/yazi.toml | 1 + yazi-config/src/preview/preview.rs | 6 ++++- yazi-plugin/preset/plugins/font.lua | 2 +- yazi-plugin/preset/plugins/image.lua | 2 +- yazi-plugin/preset/plugins/magick.lua | 2 +- yazi-plugin/preset/plugins/pdf.lua | 2 +- yazi-plugin/preset/plugins/video.lua | 2 +- yazi-plugin/src/utils/image.rs | 28 +++++++++++++++++++++++ yazi-shared/src/alignment.rs | 27 ++++++++++++++++++++++ yazi-shared/src/lib.rs | 2 +- 18 files changed, 154 insertions(+), 37 deletions(-) create mode 100644 yazi-shared/src/alignment.rs diff --git a/yazi-adapter/src/adapter.rs b/yazi-adapter/src/adapter.rs index 7f2812348..cec408c65 100644 --- a/yazi-adapter/src/adapter.rs +++ b/yazi-adapter/src/adapter.rs @@ -5,7 +5,7 @@ use ratatui::layout::Rect; use tracing::warn; use yazi_shared::env_exists; -use super::{Iip, Kgp, KgpOld}; +use super::{Iip, Image, Kgp, KgpOld}; use crate::{Chafa, Emulator, SHOWN, Sixel, TMUX, Ueberzug, WSL}; #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -36,6 +36,20 @@ impl Display for Adapter { } impl Adapter { + pub async fn image_area(self, path: &Path, max: Rect) -> Result { + if max.is_empty() { + return Ok(Rect::default()); + } + + match self { + Self::Kgp | Self::KgpOld | Self::Iip | Self::Sixel => { + Image::image_area(path, max).await.map(|(_, area)| area) + } + Self::X11 | Self::Wayland => Ueberzug::image_area(path, max).await, + Self::Chafa => Chafa::image_area(path, max).await, + } + } + pub async fn image_show(self, path: &Path, max: Rect) -> Result { if max.is_empty() { return Ok(Rect::default()); diff --git a/yazi-adapter/src/chafa.rs b/yazi-adapter/src/chafa.rs index 5fe5570f8..e776b1eb6 100644 --- a/yazi-adapter/src/chafa.rs +++ b/yazi-adapter/src/chafa.rs @@ -11,7 +11,11 @@ use crate::{Adapter, Emulator}; pub(super) struct Chafa; impl Chafa { - pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + async fn ascii_bytes_with, Rect)) -> Result>( + path: &Path, + max: Rect, + cb: F, + ) -> Result { let output = Command::new("chafa") .args([ "-f", @@ -52,16 +56,26 @@ impl Chafa { width: first.width() as u16, height: lines.len() as u16, }; + cb((lines, area)) + } - Adapter::Chafa.image_hide()?; - Adapter::shown_store(area); - Emulator::move_lock((max.x, max.y), |stderr| { - for (i, line) in lines.into_iter().enumerate() { - stderr.write_all(line)?; - queue!(stderr, MoveTo(max.x, max.y + i as u16 + 1))?; - } - Ok(area) + pub(super) async fn image_area(path: &Path, max: Rect) -> Result { + Self::ascii_bytes_with(path, max, |(_, area)| Ok(area)).await + } + + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + Self::ascii_bytes_with(path, max, |(lines, area)| { + Adapter::Chafa.image_hide()?; + Adapter::shown_store(area); + Emulator::move_lock((max.x, max.y), |stderr| { + for (i, line) in lines.into_iter().enumerate() { + stderr.write_all(line)?; + queue!(stderr, MoveTo(max.x, max.y + i as u16 + 1))?; + } + Ok(area) + }) }) + .await } pub(super) fn image_erase(area: Rect) -> Result<()> { diff --git a/yazi-adapter/src/iip.rs b/yazi-adapter/src/iip.rs index fe039d907..025470c35 100644 --- a/yazi-adapter/src/iip.rs +++ b/yazi-adapter/src/iip.rs @@ -14,8 +14,7 @@ pub(super) struct Iip; impl Iip { 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 (img, area) = Image::image_area(path, max).await?; let b = Self::encode(img).await?; Adapter::Iip.image_hide()?; diff --git a/yazi-adapter/src/image.rs b/yazi-adapter/src/image.rs index a2ba06e4d..8eb079b79 100644 --- a/yazi-adapter/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -4,6 +4,7 @@ use anyhow::Result; use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation}; use ratatui::layout::Rect; use yazi_config::{PREVIEW, TASKS}; +use yazi_shared::alignment::{HorizontalAlignment, VerticalAlignment}; use crate::Dimension; @@ -84,6 +85,32 @@ impl Image { .unwrap_or(rect) } + pub fn align_in(inner: Rect, outer: Rect) -> Rect { + let offset_x = match PREVIEW.alignment.horizontal { + HorizontalAlignment::Left => 0, + HorizontalAlignment::Center => (outer.width - inner.width) / 2, + HorizontalAlignment::Right => outer.width - inner.width, + }; + let offset_y = match PREVIEW.alignment.vertical { + VerticalAlignment::Top => 0, + VerticalAlignment::Center => (outer.height - inner.height) / 2, + VerticalAlignment::Bottom => outer.height - inner.height, + }; + Rect { + x: outer.x + offset_x, + y: outer.y + offset_y, + width: inner.width, + height: inner.height, + } + } + + #[inline] + pub(super) async fn image_area(path: &Path, max: Rect) -> Result<(DynamicImage, Rect)> { + let img = Self::downscale(path, max).await?; + let area = Self::pixel_area((img.width(), img.height()), max); + Ok((img, area)) + } + #[inline] fn filter() -> FilterType { match PREVIEW.image_filter.as_str() { diff --git a/yazi-adapter/src/kgp.rs b/yazi-adapter/src/kgp.rs index e6aa7e588..ac10cea00 100644 --- a/yazi-adapter/src/kgp.rs +++ b/yazi-adapter/src/kgp.rs @@ -314,8 +314,7 @@ pub(super) struct Kgp; impl Kgp { 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 (img, area) = Image::image_area(path, max).await?; let b1 = Self::encode(img).await?; let b2 = Self::place(&area)?; diff --git a/yazi-adapter/src/kgp_old.rs b/yazi-adapter/src/kgp_old.rs index 3d483005a..7f0872a4f 100644 --- a/yazi-adapter/src/kgp_old.rs +++ b/yazi-adapter/src/kgp_old.rs @@ -13,8 +13,7 @@ pub(super) struct KgpOld; impl KgpOld { 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 (img, area) = Image::image_area(path, max).await?; let b = Self::encode(img).await?; Adapter::KgpOld.image_hide()?; diff --git a/yazi-adapter/src/sixel.rs b/yazi-adapter/src/sixel.rs index 06a551c2a..5295bf000 100644 --- a/yazi-adapter/src/sixel.rs +++ b/yazi-adapter/src/sixel.rs @@ -13,8 +13,7 @@ pub(super) struct Sixel; impl Sixel { 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 (img, area) = Image::image_area(path, max).await?; let b = Self::encode(img).await?; Adapter::Sixel.image_hide()?; diff --git a/yazi-adapter/src/ueberzug.rs b/yazi-adapter/src/ueberzug.rs index d7a07cbb0..4acf60ce0 100644 --- a/yazi-adapter/src/ueberzug.rs +++ b/yazi-adapter/src/ueberzug.rs @@ -41,23 +41,29 @@ impl Ueberzug { DEMON.init(Some(tx)) } + pub(super) async fn image_area(path: &Path, max: Rect) -> Result { + let p = path.to_owned(); + let ImageSize { width: w, height: h } = + tokio::task::spawn_blocking(move || imagesize::size(p)).await??; + + Ok( + Dimension::ratio() + .map(|(r1, r2)| Rect { + x: max.x, + y: max.y, + width: max.width.min((w.min(PREVIEW.max_width as _) as f64 / r1).ceil() as _), + height: max.height.min((h.min(PREVIEW.max_height as _) as f64 / r2).ceil() as _), + }) + .unwrap_or(max), + ) + } + pub(super) async fn image_show(path: &Path, max: Rect) -> Result { let Some(tx) = &*DEMON else { bail!("uninitialized ueberzugpp"); }; - let p = path.to_owned(); - let ImageSize { width: w, height: h } = - tokio::task::spawn_blocking(move || imagesize::size(p)).await??; - - let area = Dimension::ratio() - .map(|(r1, r2)| Rect { - x: max.x, - y: max.y, - width: max.width.min((w.min(PREVIEW.max_width as _) as f64 / r1).ceil() as _), - height: max.height.min((h.min(PREVIEW.max_height as _) as f64 / r2).ceil() as _), - }) - .unwrap_or(max); + let area = Self::image_area(path, max).await?; tx.send(Some((path.to_owned(), area)))?; Adapter::shown_store(area); diff --git a/yazi-config/preset/yazi.toml b/yazi-config/preset/yazi.toml index 9c71da8be..1bef58211 100644 --- a/yazi-config/preset/yazi.toml +++ b/yazi-config/preset/yazi.toml @@ -21,6 +21,7 @@ wrap = "no" tab_size = 2 max_width = 600 max_height = 900 +alignment = { horizontal = "center", vertical = "top" } cache_dir = "" image_delay = 30 image_filter = "triangle" diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index f6a48264c..007e6b4ac 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, path::PathBuf, str::FromStr, time::{SystemTime, UNIX_EPOC use anyhow::Context; use serde::{Deserialize, Deserializer, Serialize}; use validator::Validate; -use yazi_shared::fs::expand_path; +use yazi_shared::{alignment::Alignment, fs::expand_path}; use super::PreviewWrap; use crate::Xdg; @@ -17,6 +17,7 @@ pub struct Preview { pub tab_size: u8, pub max_width: u32, pub max_height: u32, + pub alignment: Alignment, pub cache_dir: PathBuf, @@ -73,6 +74,8 @@ impl<'de> Deserialize<'de> for Preview { tab_size: u8, max_width: u32, max_height: u32, + #[serde(default)] + alignment: Alignment, cache_dir: Option, @@ -96,6 +99,7 @@ impl<'de> Deserialize<'de> for Preview { tab_size: preview.tab_size, max_width: preview.max_width, max_height: preview.max_height, + alignment: preview.alignment, cache_dir: preview .cache_dir diff --git a/yazi-plugin/preset/plugins/font.lua b/yazi-plugin/preset/plugins/font.lua index b0b648dcc..d651dbc8e 100644 --- a/yazi-plugin/preset/plugins/font.lua +++ b/yazi-plugin/preset/plugins/font.lua @@ -9,7 +9,7 @@ function M:peek() end ya.sleep(math.max(0, PREVIEW.image_delay / 1000 + start - os.clock())) - ya.image_show(cache, self.area) + ya.image_show_aligned(cache, self.area) ya.preview_widgets(self, {}) end diff --git a/yazi-plugin/preset/plugins/image.lua b/yazi-plugin/preset/plugins/image.lua index a5a04b412..d3a53b5cb 100644 --- a/yazi-plugin/preset/plugins/image.lua +++ b/yazi-plugin/preset/plugins/image.lua @@ -7,7 +7,7 @@ function M:peek() end ya.sleep(math.max(0, PREVIEW.image_delay / 1000 + start - os.clock())) - ya.image_show(url, self.area) + ya.image_show_aligned(url, self.area) ya.preview_widgets(self, {}) end diff --git a/yazi-plugin/preset/plugins/magick.lua b/yazi-plugin/preset/plugins/magick.lua index c432d3af6..57d6a76c8 100644 --- a/yazi-plugin/preset/plugins/magick.lua +++ b/yazi-plugin/preset/plugins/magick.lua @@ -7,7 +7,7 @@ function M:peek() end ya.sleep(math.max(0, PREVIEW.image_delay / 1000 + start - os.clock())) - ya.image_show(cache, self.area) + ya.image_show_aligned(cache, self.area) ya.preview_widgets(self, {}) end diff --git a/yazi-plugin/preset/plugins/pdf.lua b/yazi-plugin/preset/plugins/pdf.lua index b8a66a841..e3f6edad2 100644 --- a/yazi-plugin/preset/plugins/pdf.lua +++ b/yazi-plugin/preset/plugins/pdf.lua @@ -7,7 +7,7 @@ function M:peek() end ya.sleep(math.max(0, PREVIEW.image_delay / 1000 + start - os.clock())) - ya.image_show(cache, self.area) + ya.image_show_aligned(cache, self.area) ya.preview_widgets(self, {}) end diff --git a/yazi-plugin/preset/plugins/video.lua b/yazi-plugin/preset/plugins/video.lua index 74e462ff1..766e0a9d3 100644 --- a/yazi-plugin/preset/plugins/video.lua +++ b/yazi-plugin/preset/plugins/video.lua @@ -7,7 +7,7 @@ function M:peek() end ya.sleep(math.max(0, PREVIEW.image_delay / 1000 + start - os.clock())) - ya.image_show(cache, self.area) + ya.image_show_aligned(cache, self.area) ya.preview_widgets(self, {}) end diff --git a/yazi-plugin/src/utils/image.rs b/yazi-plugin/src/utils/image.rs index 5021c9ff2..57a0b1549 100644 --- a/yazi-plugin/src/utils/image.rs +++ b/yazi-plugin/src/utils/image.rs @@ -17,6 +17,34 @@ impl Utils { })?, )?; + ya.raw_set( + "image_area", + lua.create_async_function(|lua, (url, rect): (UrlRef, Rect)| async move { + if let Ok(area) = ADAPTOR.image_area(&url, *rect).await { + Rect::from(area).into_lua(&lua) + } else { + Value::Nil.into_lua(&lua) + } + })?, + )?; + + ya.raw_set( + "image_show_aligned", + lua.create_async_function(|lua, (url, rect): (UrlRef, Rect)| async move { + if let Ok(area) = ADAPTOR.image_area(&url, *rect).await { + let aligned = Image::align_in(area, *rect); + + if let Ok(area) = ADAPTOR.image_show(&url, aligned).await { + Rect::from(area).into_lua(&lua) + } else { + Value::Nil.into_lua(&lua) + } + } else { + Value::Nil.into_lua(&lua) + } + })?, + )?; + ya.raw_set( "image_precache", lua.create_async_function(|_, (src, dist): (UrlRef, UrlRef)| async move { diff --git a/yazi-shared/src/alignment.rs b/yazi-shared/src/alignment.rs new file mode 100644 index 000000000..6d5d571d8 --- /dev/null +++ b/yazi-shared/src/alignment.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum HorizontalAlignment { + Left, + #[default] + Center, + Right, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum VerticalAlignment { + #[default] + Top, + Center, + Bottom, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct Alignment { + #[serde(default)] + pub horizontal: HorizontalAlignment, + #[serde(default)] + pub vertical: VerticalAlignment, +} diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index 1dd01b9dd..0b06fe726 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -1,6 +1,6 @@ #![allow(clippy::option_map_unit_fn)] -yazi_macro::mod_pub!(errors event fs shell theme translit); +yazi_macro::mod_pub!(alignment errors event fs shell theme translit); yazi_macro::mod_flat!(chars condition debounce env id layer natsort number os rand ro_cell sync_cell terminal throttle time xdg);