diff --git a/config/yazi.toml b/config/yazi.toml index 6c7eb5c1..9682df85 100644 --- a/config/yazi.toml +++ b/config/yazi.toml @@ -4,6 +4,7 @@ sort_reverse = true show_hidden = false [preview] +adapter = "kitty" tab_size = 2 max_width = 600 max_height = 900 diff --git a/cspell.json b/cspell.json index 109e30dc..126144f9 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"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"],"version":"0.2"} +{"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"} diff --git a/src/config/preview/adapter.rs b/src/config/preview/adapter.rs new file mode 100644 index 00000000..490219e3 --- /dev/null +++ b/src/config/preview/adapter.rs @@ -0,0 +1,21 @@ +use anyhow::bail; +use serde::Deserialize; + +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(try_from = "String")] +pub enum PreviewAdapter { + Kitty, + Ueberzug, +} + +impl TryFrom for PreviewAdapter { + 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 adapter: {}", value), + }) + } +} diff --git a/src/config/preview/mod.rs b/src/config/preview/mod.rs index eacd60e4..73d684f7 100644 --- a/src/config/preview/mod.rs +++ b/src/config/preview/mod.rs @@ -1,3 +1,5 @@ +mod adapter; mod preview; +pub use adapter::*; pub use preview::*; diff --git a/src/config/preview/preview.rs b/src/config/preview/preview.rs index a471305e..03bcc32c 100644 --- a/src/config/preview/preview.rs +++ b/src/config/preview/preview.rs @@ -1,9 +1,11 @@ use serde::Deserialize; +use super::PreviewAdapter; use crate::config::MERGED_YAZI; #[derive(Debug, Deserialize)] pub struct Preview { + pub adapter: PreviewAdapter, pub tab_size: u32, pub max_width: u32, diff --git a/src/core/adapter/adapter.rs b/src/core/adapter/adapter.rs new file mode 100644 index 00000000..16d35724 --- /dev/null +++ b/src/core/adapter/adapter.rs @@ -0,0 +1,47 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use ratatui::prelude::Rect; +use tokio::sync::mpsc::UnboundedSender; + +use super::{kitty::Kitty, ueberzug::Ueberzug}; +use crate::config::{preview::PreviewAdapter, PREVIEW}; + +pub struct Adapter { + ueberzug: Option>>, +} + +impl Adapter { + pub fn new() -> Self { + let mut adapter = Self { ueberzug: None }; + + if PREVIEW.adapter == PreviewAdapter::Ueberzug { + adapter.ueberzug = Ueberzug::init().ok(); + } + + adapter + } + + pub async fn image_show(&self, path: &Path, rect: Rect) -> Result<()> { + match PREVIEW.adapter { + PreviewAdapter::Kitty => Kitty::image_show(path, rect).await, + PreviewAdapter::Ueberzug => { + if let Some(tx) = &self.ueberzug { + tx.send(Some((path.to_path_buf(), rect))).ok(); + } + Ok(()) + } + } + } + + pub fn image_hide(&self) { + match PREVIEW.adapter { + PreviewAdapter::Kitty => Kitty::image_hide(), + PreviewAdapter::Ueberzug => { + if let Some(tx) = &self.ueberzug { + tx.send(None).ok(); + } + } + } + } +} diff --git a/src/core/adapter/kitty.rs b/src/core/adapter/kitty.rs index c314f767..dc587233 100644 --- a/src/core/adapter/kitty.rs +++ b/src/core/adapter/kitty.rs @@ -1,13 +1,43 @@ -use std::io::Write; +use std::{io::Write, path::Path}; use anyhow::Result; use base64::{engine::general_purpose, Engine}; -use image::DynamicImage; +use image::{imageops::FilterType, DynamicImage}; +use ratatui::prelude::Rect; +use tokio::{fs, io::AsyncWriteExt}; + +use crate::{config::PREVIEW, misc::tty_ratio, ui::Term}; pub struct Kitty; impl Kitty { - pub fn image_show(img: DynamicImage) -> Result> { + 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??; + + Term::move_to(rect.x, rect.y).ok(); + tokio::io::stdout().write_all(&b).await.ok(); + Ok(()) + } + + #[inline] + pub fn image_hide() { std::io::stdout().write_all(b"\x1b_Ga=d\x1b\\").ok(); } + + fn encode(img: DynamicImage) -> Result> { fn output(raw: Vec, format: u8, size: (u32, u32)) -> Result> { let b64 = general_purpose::STANDARD.encode(raw).chars().collect::>(); @@ -43,7 +73,4 @@ impl Kitty { v => output(v.to_rgb8().into_raw(), 24, size), } } - - #[inline] - pub fn image_hide() -> &'static [u8; 8] { b"\x1b_Ga=d\x1b\\" } } diff --git a/src/core/adapter/mod.rs b/src/core/adapter/mod.rs index 092bf152..14345594 100644 --- a/src/core/adapter/mod.rs +++ b/src/core/adapter/mod.rs @@ -1,3 +1,5 @@ +mod adapter; mod kitty; +mod ueberzug; -pub use kitty::*; +pub use adapter::*; diff --git a/src/core/adapter/ueberzug.rs b/src/core/adapter/ueberzug.rs new file mode 100644 index 00000000..3dd5498b --- /dev/null +++ b/src/core/adapter/ueberzug.rs @@ -0,0 +1,62 @@ +use std::{path::PathBuf, process::Stdio}; + +use anyhow::{Ok, Result}; +use ratatui::prelude::Rect; +use tokio::{io::AsyncWriteExt, process::{Child, Command}, select, sync::mpsc::{self, UnboundedSender}}; + +pub struct Ueberzug; + +impl Ueberzug { + pub fn init() -> Result>> { + let mut child = Some(Self::create_demon()?); + 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 { + child = Self::create_demon().ok(); + if let Some(c) = &mut child { + Self::send_command(c, img).await.ok(); + } + } else { + break; + } + } + }); + + Ok(tx) + } + + 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()?, + ) + } + + 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?; + } else { + stdin.write_all(br#"{"action":"remove","identifier":"preview"}\n"#).await?; + } + Ok(()) + } +} diff --git a/src/core/manager/preview.rs b/src/core/manager/preview.rs index 8a0b5153..9d99a22a 100644 --- a/src/core/manager/preview.rs +++ b/src/core/manager/preview.rs @@ -1,21 +1,22 @@ -use std::{fs::File, io::{BufRead, BufReader}, path::{Path, PathBuf}, sync::OnceLock}; +use std::{fs::File, io::{BufRead, BufReader}, path::{Path, PathBuf}, sync::{Arc, OnceLock}}; use anyhow::{anyhow, Result}; -use image::imageops::FilterType; +use ratatui::prelude::Rect; use syntect::{easy::HighlightFile, highlighting::{Theme, ThemeSet}, parsing::SyntaxSet, util::as_24_bit_terminal_escaped}; use tokio::{fs, task::JoinHandle}; use super::{ALL_RATIO, PREVIEW_BORDER, PREVIEW_PADDING, PREVIEW_RATIO}; -use crate::{config::{PREVIEW, THEME}, core::{adapter::Kitty, external, files::{Files, FilesOp}, tasks::Precache}, emit, misc::{tty_ratio, tty_size, MimeKind}}; +use crate::{config::{PREVIEW, THEME}, core::{adapter::Adapter, external, files::{Files, FilesOp}, tasks::Precache}, emit, misc::{tty_size, MimeKind}}; static SYNTECT_SYNTAX: OnceLock = OnceLock::new(); static SYNTECT_THEME: OnceLock = OnceLock::new(); -#[derive(Debug)] pub struct Preview { pub path: PathBuf, pub data: PreviewData, - handle: Option>, + + handle: Option>, + adaptor: Arc, } #[derive(Debug, Default)] @@ -24,12 +25,16 @@ pub enum PreviewData { None, Folder, Text(String), - Image(Vec), } impl Preview { pub fn new() -> Self { - Self { path: Default::default(), data: Default::default(), handle: Default::default() } + Self { + path: Default::default(), + data: Default::default(), + handle: Default::default(), + adaptor: Arc::new(Adapter::new()), + } } fn size() -> (u16, u16) { @@ -43,14 +48,15 @@ impl Preview { handle.abort(); } + let adaptor = self.adaptor.clone(); let (path, mime) = (path.to_path_buf(), mime.to_owned()); self.handle = Some(tokio::spawn(async move { let result = match MimeKind::new(&mime) { MimeKind::Dir => Self::folder(&path).await, MimeKind::JSON => Self::json(&path).await.map(PreviewData::Text), MimeKind::Text => Self::highlight(&path).await.map(PreviewData::Text), - MimeKind::Image => Self::image(&path).await.map(PreviewData::Image), - MimeKind::Video => Self::video(&path).await.map(PreviewData::Image), + MimeKind::Image => Self::image(adaptor, &path).await, + MimeKind::Video => Self::video(adaptor, &path).await, MimeKind::Archive => Self::archive(&path).await.map(PreviewData::Text), MimeKind::Others => Err(anyhow!("Unsupported mimetype: {}", mime)), }; @@ -78,38 +84,29 @@ impl Preview { Ok(PreviewData::Folder) } - pub async fn image(mut path: &Path) -> Result> { + pub async fn image(adaptor: Arc, mut path: &Path) -> Result { let cache = Precache::cache(path); if fs::metadata(&cache).await.is_ok() { path = cache.as_path(); } - let (w, h) = { - let r = tty_ratio(); - let (w, h) = Self::size(); - let (w, h) = ((w as f64 * r.0) as u32, (h as f64 * r.1) as u32); - (w.min(PREVIEW.max_width), h.min(PREVIEW.max_height)) - }; - - let img = fs::read(path).await?; - tokio::task::spawn_blocking(move || -> Result> { - let img = image::load_from_memory(&img)?; - Kitty::image_show(if img.width() > w || img.height() > h { - img.resize(w, h, FilterType::Triangle) - } else { - img - }) - }) - .await? + // TODO: image + adaptor.image_show(path, Rect { + x: todo!(), + y: todo!(), + width: todo!(), + height: todo!(), + }); + Ok(PreviewData::None) } - pub async fn video(path: &Path) -> Result> { + pub async fn video(adaptor: Arc, path: &Path) -> Result { let cache = Precache::cache(path); if fs::metadata(&cache).await.is_err() { external::ffmpegthumbnailer(path, &cache).await?; } - Self::image(&cache).await + Self::image(adaptor, &cache).await } pub async fn json(path: &Path) -> Result { diff --git a/src/ui/manager/preview.rs b/src/ui/manager/preview.rs index f20eddd1..cd97c95a 100644 --- a/src/ui/manager/preview.rs +++ b/src/ui/manager/preview.rs @@ -1,10 +1,8 @@ -use std::io::{stdout, Write}; - use ansi_to_tui::IntoText; use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}}; use super::Folder; -use crate::{core::{adapter::Kitty, manager::PreviewData}, ui::{Ctx, Term}}; +use crate::{core::manager::PreviewData, ui::Ctx}; pub struct Preview<'a> { cx: &'a Ctx, @@ -16,32 +14,26 @@ impl<'a> Preview<'a> { impl<'a> Widget for Preview<'a> { fn render(self, area: Rect, buf: &mut Buffer) { - if self.cx.input.visible || self.cx.select.visible || self.cx.tasks.visible { - stdout().write(Kitty::image_hide()).ok(); - return; - } + // TODO: image + // if self.cx.input.visible || self.cx.select.visible || self.cx.tasks.visible { + // } let manager = &self.cx.manager; let hovered = if let Some(h) = manager.hovered() { - h.clone() + h.path() } else { - stdout().write(Kitty::image_hide()).ok(); return; }; let preview = manager.active().preview(); - if preview.path != hovered.path { + if preview.path != hovered { return; } - if !matches!(preview.data, PreviewData::Image(_)) { - stdout().write(Kitty::image_hide()).ok(); - } - match &preview.data { PreviewData::None => {} PreviewData::Folder => { - if let Some(folder) = manager.active().history(&hovered.path) { + if let Some(folder) = manager.active().history(&hovered) { Folder::new(self.cx, folder).with_preview(true).render(area, buf); } } @@ -49,10 +41,6 @@ impl<'a> Widget for Preview<'a> { let p = Paragraph::new(s.as_bytes().into_text().unwrap()); p.render(area, buf); } - PreviewData::Image(b) => { - Term::move_to(area.x, area.y).ok(); - stdout().write(b).ok(); - } } } }