diff --git a/Cargo.lock b/Cargo.lock index 84f49620..b4703982 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -961,9 +961,9 @@ dependencies = [ [[package]] name = "matchit" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +checksum = "67827e6ea8ee8a7c4a72227ef4fc08957040acffdb5f122733b24fa12daff41b" [[package]] name = "md5" @@ -1425,7 +1425,7 @@ checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.3", + "regex-automata 0.3.4", "regex-syntax 0.7.4", ] @@ -1440,9 +1440,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" dependencies = [ "aho-corasick", "memchr", @@ -1502,18 +1502,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.176" +version = "1.0.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76dc28c9523c5d70816e393136b86d48909cfb27cecaa902d338c19ed47164dc" +checksum = "60363bdd39a7be0266a520dab25fdc9241d2f987b08a01e01f0ec6d06a981348" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.176" +version = "1.0.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e7b8c5dc823e3b90651ff1d3808419cd14e5ad76de04feaf37da114e7a306f" +checksum = "f28482318d6641454cb273da158647922d1be6b5a2fcc6165cd89ebdd7ed576b" dependencies = [ "proc-macro2", "quote", diff --git a/config/yazi.toml b/config/yazi.toml index 03ed3be0..6c7eb5c1 100644 --- a/config/yazi.toml +++ b/config/yazi.toml @@ -4,7 +4,6 @@ sort_reverse = true show_hidden = false [preview] -adaptor = "kitty" tab_size = 2 max_width = 600 max_height = 900 diff --git a/cspell.json b/cspell.json index 126144f9..702a1da5 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug"],"version":"0.2","language":"en"} +{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"],"language":"en","version":"0.2"} diff --git a/src/config/preview/adaptor.rs b/src/config/preview/adaptor.rs index 7e1e95b4..c1cb9e26 100644 --- a/src/config/preview/adaptor.rs +++ b/src/config/preview/adaptor.rs @@ -1,21 +1,55 @@ -use anyhow::bail; -use serde::Deserialize; +use std::env; -#[derive(Debug, Deserialize, PartialEq, Eq)] -#[serde(try_from = "String")] +#[derive(Debug, PartialEq, Eq)] pub enum PreviewAdaptor { Kitty, - Ueberzug, + Iterm2, + + // Supported by Überzug++ + X11, + Wayland, + Sixel, + Chafa, } -impl TryFrom for PreviewAdaptor { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result { - Ok(match value.to_lowercase().as_str() { - "kitty" => Self::Kitty, - "ueberzug" => Self::Ueberzug, - _ => bail!("invalid preview adaptor: {}", value), - }) +impl Default for PreviewAdaptor { + fn default() -> Self { + match env::var("TERM").unwrap_or_default().as_str() { + "wezterm" => return Self::Kitty, + "xterm-kitty" => return Self::Kitty, + "iterm2" => return Self::Iterm2, + "foot" => return Self::Sixel, + _ => {} + } + match env::var("XDG_SESSION_TYPE").unwrap_or_default().as_str() { + "x11" => return Self::X11, + "wayland" => return Self::Wayland, + _ => Self::Chafa, + } + } +} + +impl ToString for PreviewAdaptor { + fn to_string(&self) -> String { + match self { + PreviewAdaptor::Kitty => "kitty", + PreviewAdaptor::Iterm2 => "iterm2", + PreviewAdaptor::X11 => "x11", + PreviewAdaptor::Wayland => "wayland", + PreviewAdaptor::Sixel => "sixel", + PreviewAdaptor::Chafa => "chafa", + } + .to_string() + } +} + +impl PreviewAdaptor { + #[inline] + pub fn needs_ueberzug(&self) -> bool { + match self { + PreviewAdaptor::Kitty => false, + PreviewAdaptor::Iterm2 => false, + _ => true, + } } } diff --git a/src/config/preview/preview.rs b/src/config/preview/preview.rs index d33272e2..613ca510 100644 --- a/src/config/preview/preview.rs +++ b/src/config/preview/preview.rs @@ -5,6 +5,7 @@ use crate::config::MERGED_YAZI; #[derive(Debug, Deserialize)] pub struct Preview { + #[serde(skip)] pub adaptor: PreviewAdaptor, pub tab_size: u32, diff --git a/src/core/adaptor/adaptor.rs b/src/core/adaptor/adaptor.rs index 11eb53db..9fda1726 100644 --- a/src/core/adaptor/adaptor.rs +++ b/src/core/adaptor/adaptor.rs @@ -4,7 +4,7 @@ use anyhow::Result; use ratatui::prelude::Rect; use tokio::sync::mpsc::UnboundedSender; -use super::{kitty::Kitty, ueberzug::Ueberzug}; +use super::{iterm2::Iterm2, kitty::Kitty, ueberzug::Ueberzug}; use crate::config::{preview::PreviewAdaptor, PREVIEW}; pub struct Adaptor { @@ -15,7 +15,7 @@ impl Adaptor { pub fn new() -> Self { let mut adaptor = Self { ueberzug: None }; - if PREVIEW.adaptor == PreviewAdaptor::Ueberzug { + if PREVIEW.adaptor.needs_ueberzug() { adaptor.ueberzug = Ueberzug::init().ok(); } @@ -25,7 +25,8 @@ impl Adaptor { pub async fn image_show(&self, path: &Path, rect: Rect) -> Result<()> { match PREVIEW.adaptor { PreviewAdaptor::Kitty => Kitty::image_show(path, rect).await, - PreviewAdaptor::Ueberzug => { + PreviewAdaptor::Iterm2 => Iterm2::image_show(path, rect).await, + _ => { if let Some(tx) = &self.ueberzug { tx.send(Some((path.to_path_buf(), rect))).ok(); } @@ -37,7 +38,8 @@ impl Adaptor { pub fn image_hide(&self) { match PREVIEW.adaptor { PreviewAdaptor::Kitty => Kitty::image_hide(), - PreviewAdaptor::Ueberzug => { + PreviewAdaptor::Iterm2 => {} + _ => { if let Some(tx) = &self.ueberzug { tx.send(None).ok(); } diff --git a/src/core/adaptor/image.rs b/src/core/adaptor/image.rs new file mode 100644 index 00000000..65e1b26f --- /dev/null +++ b/src/core/adaptor/image.rs @@ -0,0 +1,31 @@ +use std::path::Path; + +use anyhow::Result; +use image::{imageops::FilterType, DynamicImage}; +use tokio::fs; + +use crate::{config::PREVIEW, misc::tty_ratio}; + +pub(super) struct Image; + +impl Image { + pub(super) async fn resize(path: &Path, size: (u16, u16)) -> Result { + let (w, h) = { + let r = tty_ratio(); + let (w, h) = ((size.0 as f64 * r.0) as u32, (size.1 as f64 * r.1) as u32); + (w.min(PREVIEW.max_width), h.min(PREVIEW.max_height)) + }; + + let img = fs::read(path).await?; + let img = tokio::task::spawn_blocking(move || -> Result { + let img = image::load_from_memory(&img)?; + Ok(if img.width() > w || img.height() > h { + img.resize(w, h, FilterType::Triangle) + } else { + img + }) + }); + + Ok(img.await??) + } +} diff --git a/src/core/adaptor/iterm2.rs b/src/core/adaptor/iterm2.rs new file mode 100644 index 00000000..fd5766fc --- /dev/null +++ b/src/core/adaptor/iterm2.rs @@ -0,0 +1,50 @@ +use std::{io::Write, path::Path}; + +use anyhow::Result; +use base64::{engine::general_purpose, Engine}; +use image::DynamicImage; +use ratatui::prelude::Rect; +use tokio::io::AsyncWriteExt; + +use super::image::Image; +use crate::ui::Term; + +pub(super) struct Iterm2; + +impl Iterm2 { + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { + let img = Image::resize(path, (rect.width, rect.height)).await?; + let b = Self::encode(img).await?; + + Term::move_to(rect.x, rect.y).ok(); + tokio::io::stdout().write_all(&b).await.ok(); + Ok(()) + } + + async fn encode(img: DynamicImage) -> Result> { + fn output(raw: Vec, size: (u32, u32)) -> Result> { + let mut buf = Vec::with_capacity(raw.len() * 4 / 3 + 4 + 200); + + write!( + buf, + "\x1b]1337;File=inline=1;size={};width={}px;height={}px:", + raw.len(), + size.0, + size.1, + )?; + + let len = buf.len(); + let written = general_purpose::STANDARD.encode_slice(raw, &mut buf[len..])?; + buf.truncate(len + written); + Ok(buf) + } + + let size = (img.width(), img.height()); + tokio::task::spawn_blocking(move || match img { + DynamicImage::ImageRgb8(v) => output(v.into_raw(), size), + DynamicImage::ImageRgba8(v) => output(v.into_raw(), size), + v => output(v.to_rgb8().into_raw(), size), + }) + .await? + } +} diff --git a/src/core/adaptor/kitty.rs b/src/core/adaptor/kitty.rs index 540458d4..fa41c096 100644 --- a/src/core/adaptor/kitty.rs +++ b/src/core/adaptor/kitty.rs @@ -2,32 +2,19 @@ use std::{io::Write, path::Path}; use anyhow::Result; use base64::{engine::general_purpose, Engine}; -use image::{imageops::FilterType, DynamicImage}; +use image::DynamicImage; use ratatui::prelude::Rect; -use tokio::{fs, io::AsyncWriteExt}; +use tokio::io::AsyncWriteExt; -use crate::{config::PREVIEW, misc::tty_ratio, ui::Term}; +use super::image::Image; +use crate::ui::Term; -pub struct Kitty; +pub(super) struct Kitty; impl Kitty { - pub async fn image_show(path: &Path, rect: Rect) -> Result<()> { - let (w, h) = { - let r = tty_ratio(); - let (w, h) = ((rect.width as f64 * r.0) as u32, (rect.height as f64 * r.1) as u32); - (w.min(PREVIEW.max_width), h.min(PREVIEW.max_height)) - }; - - let img = fs::read(path).await?; - let b = tokio::task::spawn_blocking(move || -> Result> { - let img = image::load_from_memory(&img)?; - Self::encode(if img.width() > w || img.height() > h { - img.resize(w, h, FilterType::Triangle) - } else { - img - }) - }) - .await??; + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { + let img = Image::resize(path, (rect.width, rect.height)).await?; + let b = Self::encode(img).await?; Term::move_to(rect.x, rect.y).ok(); tokio::io::stdout().write_all(&b).await.ok(); @@ -35,9 +22,9 @@ impl Kitty { } #[inline] - pub fn image_hide() { std::io::stdout().write_all(b"\x1b\\\x1b_Ga=d\x1b\\").ok(); } + pub(super) fn image_hide() { std::io::stdout().write_all(b"\x1b\\\x1b_Ga=d\x1b\\").ok(); } - fn encode(img: DynamicImage) -> Result> { + async fn encode(img: DynamicImage) -> Result> { fn output(raw: Vec, format: u8, size: (u32, u32)) -> Result> { let b64 = general_purpose::STANDARD.encode(raw).chars().collect::>(); @@ -67,10 +54,11 @@ impl Kitty { } let size = (img.width(), img.height()); - match img { + tokio::task::spawn_blocking(move || match img { DynamicImage::ImageRgb8(v) => output(v.into_raw(), 24, size), DynamicImage::ImageRgba8(v) => output(v.into_raw(), 32, size), v => output(v.to_rgb8().into_raw(), 24, size), - } + }) + .await? } } diff --git a/src/core/adaptor/mod.rs b/src/core/adaptor/mod.rs index 9e4d7a07..1c99201e 100644 --- a/src/core/adaptor/mod.rs +++ b/src/core/adaptor/mod.rs @@ -1,4 +1,6 @@ mod adaptor; +mod image; +mod iterm2; mod kitty; mod ueberzug; diff --git a/src/core/adaptor/ueberzug.rs b/src/core/adaptor/ueberzug.rs index 3dd5498b..84ea2ddf 100644 --- a/src/core/adaptor/ueberzug.rs +++ b/src/core/adaptor/ueberzug.rs @@ -1,36 +1,29 @@ use std::{path::PathBuf, process::Stdio}; -use anyhow::{Ok, Result}; +use anyhow::Result; use ratatui::prelude::Rect; -use tokio::{io::AsyncWriteExt, process::{Child, Command}, select, sync::mpsc::{self, UnboundedSender}}; +use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}}; -pub struct Ueberzug; +use crate::config::PREVIEW; + +pub(super) struct Ueberzug; impl Ueberzug { - pub fn init() -> Result>> { - let mut child = Some(Self::create_demon()?); + pub(super) fn init() -> Result>> { + let mut child = Self::create_demon().ok(); let (tx, mut rx) = mpsc::unbounded_channel(); tokio::spawn(async move { - loop { - if let Some(c) = &mut child { - select! { - _ = c.wait() => child = None, - data = rx.recv() => { - if let Some(img) = data { - Self::send_command(c, img).await.ok(); - } else { - break; - } - }, - } - } else if let Some(img) = rx.recv().await { + while let Some(img) = rx.recv().await { + let exit = child.as_mut().and_then(|c| c.try_wait().ok()); + if exit != Some(None) { + child = None; + } + if child.is_none() { child = Self::create_demon().ok(); - if let Some(c) = &mut child { - Self::send_command(c, img).await.ok(); - } - } else { - break; + } + if let Some(c) = &mut child { + Self::send_command(c, img).await.ok(); } } }); @@ -41,21 +34,31 @@ impl Ueberzug { fn create_demon() -> Result { Ok( Command::new("ueberzug") - .args(["layer", "-s", "--use-escape-codes"]) - .kill_on_drop(true) - .stdin(Stdio::piped()) - // .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn()?, + .args(["layer", "-so", &PREVIEW.adaptor.to_string()]) + .kill_on_drop(true) + .stdin(Stdio::piped()) + .stderr(Stdio::null()) + .spawn()?, ) } async fn send_command(child: &mut Child, img: Option<(PathBuf, Rect)>) -> Result<()> { let stdin = child.stdin.as_mut().unwrap(); if let Some((path, rect)) = img { - stdin.write_all(br#"{"action":"add","identifier":"preview","max_height":0,"max_width":0,"path":"/Users/ika/Downloads/photo_2023-06-28 07.23.33.jpeg","x":0,"y":0}\n"#).await?; + let cmd = format!( + r#"{{"action":"add","identifier":"yazi","x":{},"y":{},"max_width":{},"max_height":{},"path":"{}"}}{}"#, + rect.x, + rect.y, + rect.width, + rect.height, + path.to_string_lossy(), + "\n" + ); + stdin.write_all(cmd.as_bytes()).await?; } else { - stdin.write_all(br#"{"action":"remove","identifier":"preview"}\n"#).await?; + stdin + .write_all(format!(r#"{{"action":"remove","identifier":"yazi"}}{}"#, "\n").as_bytes()) + .await?; } Ok(()) } diff --git a/src/ui/manager/preview.rs b/src/ui/manager/preview.rs index cd97c95a..f3a0b92c 100644 --- a/src/ui/manager/preview.rs +++ b/src/ui/manager/preview.rs @@ -1,5 +1,5 @@ use ansi_to_tui::IntoText; -use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}}; +use ratatui::{buffer::Buffer, layout::Rect, widgets::{Clear, Paragraph, Widget}}; use super::Folder; use crate::{core::manager::PreviewData, ui::Ctx}; @@ -14,6 +14,8 @@ impl<'a> Preview<'a> { impl<'a> Widget for Preview<'a> { fn render(self, area: Rect, buf: &mut Buffer) { + Clear.render(Rect { x: area.x, y: area.y, width: area.width + 1, height: area.height }, buf); + // TODO: image // if self.cx.input.visible || self.cx.select.visible || self.cx.tasks.visible { // }