diff --git a/yazi-adapter/src/adapter.rs b/yazi-adapter/src/adapter.rs index 7f2812348..d2c5a20cd 100644 --- a/yazi-adapter/src/adapter.rs +++ b/yazi-adapter/src/adapter.rs @@ -6,7 +6,7 @@ use tracing::warn; use yazi_shared::env_exists; use super::{Iip, Kgp, KgpOld}; -use crate::{Chafa, Emulator, SHOWN, Sixel, TMUX, Ueberzug, WSL}; +use crate::{Chafa, Emulator, Offset, SHOWN, Sixel, TMUX, Ueberzug, WSL}; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Adapter { @@ -36,18 +36,18 @@ impl Display for Adapter { } impl Adapter { - pub async fn image_show(self, path: &Path, max: Rect) -> Result { + pub async fn image_show(self, path: &Path, max: Rect, offset: Option) -> Result { if max.is_empty() { return Ok(Rect::default()); } match self { - Self::Kgp => Kgp::image_show(path, max).await, - Self::KgpOld => KgpOld::image_show(path, max).await, - Self::Iip => Iip::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, + Self::Kgp => Kgp::image_show(path, max, offset).await, + Self::KgpOld => KgpOld::image_show(path, max, offset).await, + Self::Iip => Iip::image_show(path, max, offset).await, + Self::Sixel => Sixel::image_show(path, max, offset).await, + Self::X11 | Self::Wayland => Ueberzug::image_show(path, max, offset).await, + Self::Chafa => Chafa::image_show(path, max, offset).await, } } diff --git a/yazi-adapter/src/chafa.rs b/yazi-adapter/src/chafa.rs index 5fe5570f8..45281f714 100644 --- a/yazi-adapter/src/chafa.rs +++ b/yazi-adapter/src/chafa.rs @@ -3,15 +3,15 @@ use std::{io::Write, path::Path, process::Stdio}; use ansi_to_tui::IntoText; use anyhow::{Result, bail}; use crossterm::{cursor::MoveTo, queue}; -use ratatui::layout::Rect; +use ratatui::layout::{Rect, Size}; use tokio::process::Command; -use crate::{Adapter, Emulator}; +use crate::{Adapter, Emulator, Offset}; pub(super) struct Chafa; impl Chafa { - pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + pub(super) async fn image_show(path: &Path, max: Rect, offset: Option) -> Result { let output = Command::new("chafa") .args([ "-f", @@ -46,11 +46,13 @@ impl Chafa { bail!("failed to parse chafa output"); }; - let area = Rect { - x: max.x, - y: max.y, - width: first.width() as u16, - height: lines.len() as u16, + let area = { + let width = first.width() as u16; + let height = lines.len() as u16; + let offset = offset.unwrap_or_else(|| { + Offset::from((Size { width, height }, Size { width: max.width, height: max.height })) + }); + Rect { x: max.x + offset.x, y: max.y + offset.y, width, height } }; Adapter::Chafa.image_hide()?; diff --git a/yazi-adapter/src/iip.rs b/yazi-adapter/src/iip.rs index fe039d907..16e7cffd3 100644 --- a/yazi-adapter/src/iip.rs +++ b/yazi-adapter/src/iip.rs @@ -8,14 +8,14 @@ use ratatui::layout::Rect; use yazi_config::PREVIEW; use super::image::Image; -use crate::{CLOSE, Emulator, START, adapter::Adapter}; +use crate::{CLOSE, Emulator, Offset, START, adapter::Adapter}; pub(super) struct Iip; impl Iip { - pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + pub(super) async fn image_show(path: &Path, max: Rect, offset: Option) -> Result { let img = Image::downscale(path, max).await?; - let area = Image::pixel_area((img.width(), img.height()), max); + let area = Image::pixel_area((img.width(), img.height()), max, offset); 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..4227761e2 100644 --- a/yazi-adapter/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -2,10 +2,10 @@ use std::path::{Path, PathBuf}; 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 ratatui::layout::{Rect, Size}; use yazi_config::{PREVIEW, TASKS}; -use crate::Dimension; +use crate::{Dimension, Offset}; pub struct Image; @@ -73,13 +73,15 @@ impl Image { .unwrap_or((PREVIEW.max_width, PREVIEW.max_height)) } - pub(super) fn pixel_area(size: (u32, u32), rect: Rect) -> Rect { + pub(super) fn pixel_area(size: (u32, u32), rect: Rect, offset: Option) -> Rect { Dimension::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, + .map(|(r1, r2)| { + let width = (size.0 as f64 / r1).ceil() as u16; + let height = (size.1 as f64 / r2).ceil() as u16; + let offset = offset.unwrap_or_else(|| { + Offset::from((Size { width, height }, Size { width: rect.width, height: rect.height })) + }); + Rect { x: rect.x + offset.x, y: rect.y + offset.y, width, height } }) .unwrap_or(rect) } diff --git a/yazi-adapter/src/kgp.rs b/yazi-adapter/src/kgp.rs index e6aa7e588..c9559512f 100644 --- a/yazi-adapter/src/kgp.rs +++ b/yazi-adapter/src/kgp.rs @@ -8,7 +8,7 @@ use image::DynamicImage; use ratatui::layout::Rect; use super::image::Image; -use crate::{CLOSE, ESCAPE, Emulator, START, adapter::Adapter}; +use crate::{CLOSE, ESCAPE, Emulator, Offset, START, adapter::Adapter}; static DIACRITICS: [char; 297] = [ '\u{0305}', @@ -313,9 +313,9 @@ static DIACRITICS: [char; 297] = [ pub(super) struct Kgp; impl Kgp { - pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + pub(super) async fn image_show(path: &Path, max: Rect, offset: Option) -> Result { let img = Image::downscale(path, max).await?; - let area = Image::pixel_area((img.width(), img.height()), max); + let area = Image::pixel_area((img.width(), img.height()), max, offset); 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..2075e3771 100644 --- a/yazi-adapter/src/kgp_old.rs +++ b/yazi-adapter/src/kgp_old.rs @@ -7,14 +7,14 @@ use image::DynamicImage; use ratatui::layout::Rect; use super::image::Image; -use crate::{CLOSE, ESCAPE, Emulator, START, adapter::Adapter}; +use crate::{CLOSE, ESCAPE, Emulator, Offset, START, adapter::Adapter}; pub(super) struct KgpOld; impl KgpOld { - pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + pub(super) async fn image_show(path: &Path, max: Rect, offset: Option) -> Result { let img = Image::downscale(path, max).await?; - let area = Image::pixel_area((img.width(), img.height()), max); + let area = Image::pixel_area((img.width(), img.height()), max, offset); let b = Self::encode(img).await?; Adapter::KgpOld.image_hide()?; diff --git a/yazi-adapter/src/lib.rs b/yazi-adapter/src/lib.rs index a98791855..0f8e055c0 100644 --- a/yazi-adapter/src/lib.rs +++ b/yazi-adapter/src/lib.rs @@ -1,7 +1,7 @@ #![allow(clippy::unit_arg)] yazi_macro::mod_flat!( - adapter chafa dimension emulator iip image kgp kgp_old mux sixel ueberzug + adapter chafa dimension emulator iip image kgp kgp_old mux offset sixel ueberzug ); use yazi_shared::{RoCell, SyncCell, env_exists, in_wsl}; diff --git a/yazi-adapter/src/offset.rs b/yazi-adapter/src/offset.rs new file mode 100644 index 000000000..54f4c3a18 --- /dev/null +++ b/yazi-adapter/src/offset.rs @@ -0,0 +1,27 @@ +use ratatui::layout::Size; +use yazi_config::PREVIEW; +use yazi_shared::alignment::{HorizontalAlignment, VerticalAlignment}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Offset { + pub x: u16, + pub y: u16, +} + +impl From<(Size, Size)> for Offset { + fn from(value: (Size, Size)) -> Self { + let inner = value.0; + let outer = value.1; + 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, + }; + Self { x: offset_x, y: offset_y } + } +} diff --git a/yazi-adapter/src/sixel.rs b/yazi-adapter/src/sixel.rs index 06a551c2a..2e23c8723 100644 --- a/yazi-adapter/src/sixel.rs +++ b/yazi-adapter/src/sixel.rs @@ -7,14 +7,14 @@ use image::DynamicImage; use ratatui::layout::Rect; use yazi_config::PREVIEW; -use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter}; +use crate::{CLOSE, ESCAPE, Emulator, Image, Offset, START, adapter::Adapter}; pub(super) struct Sixel; impl Sixel { - pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + pub(super) async fn image_show(path: &Path, max: Rect, offset: Option) -> Result { let img = Image::downscale(path, max).await?; - let area = Image::pixel_area((img.width(), img.height()), max); + let area = Image::pixel_area((img.width(), img.height()), max, offset); 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..91c7881af 100644 --- a/yazi-adapter/src/ueberzug.rs +++ b/yazi-adapter/src/ueberzug.rs @@ -2,13 +2,13 @@ use std::{path::{Path, PathBuf}, process::Stdio}; use anyhow::{Result, bail}; use imagesize::ImageSize; -use ratatui::layout::Rect; +use ratatui::layout::{Rect, Size}; use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}}; use tracing::{debug, warn}; use yazi_config::PREVIEW; use yazi_shared::{RoCell, env_exists}; -use crate::{Adapter, Dimension}; +use crate::{Adapter, Dimension, Offset}; #[allow(clippy::type_complexity)] static DEMON: RoCell>>> = RoCell::new(); @@ -41,7 +41,7 @@ impl Ueberzug { DEMON.init(Some(tx)) } - pub(super) async fn image_show(path: &Path, max: Rect) -> Result { + pub(super) async fn image_show(path: &Path, max: Rect, offset: Option) -> Result { let Some(tx) = &*DEMON else { bail!("uninitialized ueberzugpp"); }; @@ -51,11 +51,13 @@ impl Ueberzug { 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 _), + .map(|(r1, r2)| { + let width = max.width.min((w.min(PREVIEW.max_width as _) as f64 / r1).ceil() as _); + let height = max.height.min((h.min(PREVIEW.max_height as _) as f64 / r2).ceil() as _); + let offset = offset.unwrap_or_else(|| { + Offset::from((Size { width, height }, Size { width: max.width, height: max.height })) + }); + Rect { x: max.x + offset.x, y: max.y + offset.y, width, height } }) .unwrap_or(max); 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..5f5374523 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/src/utils/image.rs b/yazi-plugin/src/utils/image.rs index 5021c9ff2..58df48a39 100644 --- a/yazi-plugin/src/utils/image.rs +++ b/yazi-plugin/src/utils/image.rs @@ -1,5 +1,5 @@ use mlua::{IntoLua, Lua, Table, Value}; -use yazi_adapter::{ADAPTOR, Image}; +use yazi_adapter::{ADAPTOR, Image, Offset}; use super::Utils; use crate::{elements::Rect, url::UrlRef}; @@ -8,13 +8,19 @@ 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, Rect)| async move { - if let Ok(area) = ADAPTOR.image_show(&url, *rect).await { - Rect::from(area).into_lua(&lua) - } else { - Value::Nil.into_lua(&lua) - } - })?, + lua.create_async_function( + |lua, (url, rect, offset_table): (UrlRef, Rect, Option)| async move { + let offset = offset_table.map(|lua_offset| Offset { + x: lua_offset.get("x").unwrap_or(0), + y: lua_offset.get("y").unwrap_or(0), + }); + if let Ok(area) = ADAPTOR.image_show(&url, *rect, offset).await { + Rect::from(area).into_lua(&lua) + } else { + Value::Nil.into_lua(&lua) + } + }, + )?, )?; ya.raw_set( 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);