Skip to content

Commit

Permalink
feat: support customizable alignment for image display
Browse files Browse the repository at this point in the history
This is an attempt to provide a possible solution to sxyazi#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?
  • Loading branch information
gaesa committed Nov 10, 2024
1 parent a9a8a19 commit 1b97e51
Show file tree
Hide file tree
Showing 18 changed files with 154 additions and 37 deletions.
16 changes: 15 additions & 1 deletion yazi-adapter/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -36,6 +36,20 @@ impl Display for Adapter {
}

impl Adapter {
pub async fn image_area(self, path: &Path, max: Rect) -> Result<Rect> {
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<Rect> {
if max.is_empty() {
return Ok(Rect::default());
Expand Down
32 changes: 23 additions & 9 deletions yazi-adapter/src/chafa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Rect> {
async fn ascii_bytes_with<F: Fn((Vec<&[u8]>, Rect)) -> Result<Rect>>(
path: &Path,
max: Rect,
cb: F,
) -> Result<Rect> {
let output = Command::new("chafa")
.args([
"-f",
Expand Down Expand Up @@ -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<Rect> {
Self::ascii_bytes_with(path, max, |(_, area)| Ok(area)).await
}

pub(super) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
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<()> {
Expand Down
3 changes: 1 addition & 2 deletions yazi-adapter/src/iip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ pub(super) struct Iip;

impl Iip {
pub(super) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
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()?;
Expand Down
27 changes: 27 additions & 0 deletions yazi-adapter/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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() {
Expand Down
3 changes: 1 addition & 2 deletions yazi-adapter/src/kgp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,7 @@ pub(super) struct Kgp;

impl Kgp {
pub(super) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
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)?;
Expand Down
3 changes: 1 addition & 2 deletions yazi-adapter/src/kgp_old.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ pub(super) struct KgpOld;

impl KgpOld {
pub(super) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
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()?;
Expand Down
3 changes: 1 addition & 2 deletions yazi-adapter/src/sixel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ pub(super) struct Sixel;

impl Sixel {
pub(super) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
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()?;
Expand Down
30 changes: 18 additions & 12 deletions yazi-adapter/src/ueberzug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,29 @@ impl Ueberzug {
DEMON.init(Some(tx))
}

pub(super) async fn image_area(path: &Path, max: Rect) -> Result<Rect> {
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<Rect> {
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);
Expand Down
1 change: 1 addition & 0 deletions yazi-config/preset/yazi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion yazi-config/src/preview/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,

Expand Down Expand Up @@ -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<String>,

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion yazi-plugin/preset/plugins/font.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion yazi-plugin/preset/plugins/image.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion yazi-plugin/preset/plugins/magick.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion yazi-plugin/preset/plugins/pdf.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion yazi-plugin/preset/plugins/video.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 28 additions & 0 deletions yazi-plugin/src/utils/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 27 additions & 0 deletions yazi-shared/src/alignment.rs
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 1 addition & 1 deletion yazi-shared/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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);

Expand Down

0 comments on commit 1b97e51

Please sign in to comment.