From e6fccf9d17b5ea7bfc74a309213e3fcd04db1345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Wed, 13 Sep 2023 22:44:27 +0800 Subject: [PATCH] feat: support for image preview within tmux (#147) --- README.md | 2 + adaptor/src/adaptor.rs | 125 +++++++++++++++++++++++---- adaptor/src/iterm2.rs | 7 +- adaptor/src/kitty.rs | 17 +++- adaptor/src/lib.rs | 25 ++++-- adaptor/src/sixel.rs | 6 +- adaptor/src/ueberzug.rs | 13 +-- config/src/preview/adaptor.rs | 64 -------------- config/src/preview/mod.rs | 2 - config/src/preview/preview.rs | 5 -- core/src/manager/preview/preview.rs | 4 +- core/src/manager/preview/provider.rs | 4 +- shared/src/ro_cell.rs | 9 +- 13 files changed, 167 insertions(+), 116 deletions(-) delete mode 100644 config/src/preview/adaptor.rs diff --git a/README.md b/README.md index 82eab05f..aa2c8e42 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,8 @@ That's relying on the `$TERM`, `$TERM_PROGRAM`, and `$XDG_SESSION_TYPE` variable For instance, if your terminal is Alacritty, which doesn't support displaying images itself, but you are running on an X11/Wayland environment, it will automatically use the "Window system protocol" to display images -- this requires you to have [Überzug++](https://github.com/jstkdng/ueberzugpp) installed. +Here is a guide for tmux users: [Image preview within tmux](https://github.com/sxyazi/yazi/wiki/Image-preview-within-tmux) + ## TODO See [Feature requests](https://github.com/sxyazi/yazi/issues/51) for more details. diff --git a/adaptor/src/adaptor.rs b/adaptor/src/adaptor.rs index fde84ada..664ee78a 100644 --- a/adaptor/src/adaptor.rs +++ b/adaptor/src/adaptor.rs @@ -1,54 +1,143 @@ -use std::{path::{Path, PathBuf}, sync::atomic::{AtomicBool, Ordering}}; +use std::{env, path::{Path, PathBuf}, sync::atomic::{AtomicBool, Ordering}}; -use anyhow::Result; -use config::{preview::PreviewAdaptor, PREVIEW}; +use anyhow::{anyhow, Result}; +use config::PREVIEW; use ratatui::prelude::Rect; use shared::RoCell; use tokio::{fs, sync::mpsc::UnboundedSender}; use super::{Iterm2, Kitty}; -use crate::Sixel; +use crate::{ueberzug::Ueberzug, Sixel, TMUX}; static IMAGE_SHOWN: AtomicBool = AtomicBool::new(false); #[allow(clippy::type_complexity)] -pub(super) static UEBERZUG: RoCell>>> = - RoCell::new(); +static UEBERZUG: RoCell>>> = RoCell::new(); -pub struct Adaptor; +#[derive(Clone, Copy)] +pub enum Adaptor { + Kitty, + Iterm2, + Sixel, + + // Supported by Überzug++ + X11, + Wayland, + Chafa, +} impl Adaptor { - pub async fn image_show(mut path: &Path, rect: Rect) -> Result<()> { + pub(super) fn detect() -> Self { + let vars = [ + ("KITTY_WINDOW_ID", Self::Kitty), + ("KONSOLE_VERSION", Self::Kitty), + ("ITERM_SESSION_ID", Self::Iterm2), + ("WEZTERM_EXECUTABLE", cfg!(windows).then_some(Self::Iterm2).unwrap_or(Self::Kitty)), + ("VSCODE_INJECTION", Self::Sixel), + ]; + if let Some(var) = vars.iter().find(|v| env::var_os(v.0).is_some()) { + return var.1; + } + + let (term, program) = Self::term_program(); + match program.as_str() { + "iTerm.app" => return Self::Iterm2, + "WezTerm" => return cfg!(windows).then_some(Self::Iterm2).unwrap_or(Self::Kitty), + "BlackBox" => return Self::Sixel, + "vscode" => return Self::Sixel, + "Hyper" => return Self::Sixel, + "mintty" => return Self::Iterm2, + _ => {} + } + match term.as_str() { + "xterm-kitty" => return Self::Kitty, + "foot" => return Self::Sixel, + _ => {} + } + match env::var("XDG_SESSION_TYPE").unwrap_or_default().as_str() { + "x11" => Self::X11, + "wayland" => Self::Wayland, + _ => Self::Chafa, + } + } + + pub(super) fn term_program() -> (String, String) { + fn tmux_env(name: &str) -> Result { + let output = std::process::Command::new("tmux").args(["show-environment", name]).output()?; + + String::from_utf8(output.stdout)? + .trim() + .strip_prefix(&format!("{}=", name)) + .map_or_else(|| Err(anyhow!("")), |s| Ok(s.to_string())) + } + + let mut term = env::var("TERM").unwrap_or_default(); + let mut program = env::var("TERM_PROGRAM").unwrap_or_default(); + + if *TMUX { + term = tmux_env("TERM").unwrap_or(term); + program = tmux_env("TERM_PROGRAM").unwrap_or(program); + } + + (term, program) + } +} + +impl ToString for Adaptor { + fn to_string(&self) -> String { + match self { + Self::Kitty => "kitty", + Self::Iterm2 => "iterm2", + Self::Sixel => "sixel", + Self::X11 => "x11", + Self::Wayland => "wayland", + Self::Chafa => "chafa", + } + .to_string() + } +} + +impl Adaptor { + pub(super) fn start(self) { + UEBERZUG.init(if self.needs_ueberzug() { Ueberzug::start(self).ok() } else { None }); + } + + pub async fn image_show(self, mut path: &Path, rect: Rect) -> Result<()> { let cache = PREVIEW.cache(path, 0); if fs::metadata(&cache).await.is_ok() { path = cache.as_path(); } - Self::image_hide(rect).ok(); + self.image_hide(rect).ok(); IMAGE_SHOWN.store(true, Ordering::Relaxed); - match PREVIEW.adaptor { - PreviewAdaptor::Kitty => Kitty::image_show(path, rect).await, - PreviewAdaptor::Iterm2 => Iterm2::image_show(path, rect).await, - PreviewAdaptor::Sixel => Sixel::image_show(path, rect).await, + match self { + Self::Kitty => Kitty::image_show(path, rect).await, + Self::Iterm2 => Iterm2::image_show(path, rect).await, + Self::Sixel => Sixel::image_show(path, rect).await, _ => Ok(if let Some(tx) = &*UEBERZUG { tx.send(Some((path.to_path_buf(), rect)))?; }), } } - pub fn image_hide(rect: Rect) -> Result<()> { + pub fn image_hide(self, rect: Rect) -> Result<()> { if !IMAGE_SHOWN.swap(false, Ordering::Relaxed) { return Ok(()); } - match PREVIEW.adaptor { - PreviewAdaptor::Kitty => Kitty::image_hide(), - PreviewAdaptor::Iterm2 => Iterm2::image_hide(rect), - PreviewAdaptor::Sixel => Sixel::image_hide(rect), + match self { + Self::Kitty => Kitty::image_hide(), + Self::Iterm2 => Iterm2::image_hide(rect), + Self::Sixel => Sixel::image_hide(rect), _ => Ok(if let Some(tx) = &*UEBERZUG { tx.send(None)?; }), } } + + #[inline] + pub(super) fn needs_ueberzug(self) -> bool { + !matches!(self, Self::Kitty | Self::Iterm2 | Self::Sixel) + } } diff --git a/adaptor/src/iterm2.rs b/adaptor/src/iterm2.rs index b03821fe..fc042e28 100644 --- a/adaptor/src/iterm2.rs +++ b/adaptor/src/iterm2.rs @@ -7,6 +7,7 @@ use ratatui::prelude::Rect; use shared::Term; use super::image::Image; +use crate::{CLOSE, START}; pub(super) struct Iterm2; @@ -40,11 +41,13 @@ impl Iterm2 { let mut buf = vec![]; write!( buf, - "\x1b]1337;File=inline=1;size={};width={}px;height={}px;doNotMoveCursor=1:{}\x07", + "{}]1337;File=inline=1;size={};width={}px;height={}px;doNotMoveCursor=1:{}\x07{}", + START, jpg.len(), size.0, size.1, - general_purpose::STANDARD.encode(&jpg) + general_purpose::STANDARD.encode(&jpg), + CLOSE )?; Ok(buf) }) diff --git a/adaptor/src/kitty.rs b/adaptor/src/kitty.rs index d07a8cca..fbd2980f 100644 --- a/adaptor/src/kitty.rs +++ b/adaptor/src/kitty.rs @@ -7,6 +7,7 @@ use ratatui::prelude::Rect; use shared::Term; use super::image::Image; +use crate::{CLOSE, ESCAPE, START}; pub(super) struct Kitty; @@ -22,7 +23,7 @@ impl Kitty { #[inline] pub(super) fn image_hide() -> Result<()> { let mut stdout = stdout().lock(); - stdout.write_all(b"\x1b_Ga=d,d=A\x1b\\")?; + stdout.write_all(format!("{}_Ga=d,d=A{}\\{}", START, ESCAPE, CLOSE).as_bytes())?; stdout.flush()?; Ok(()) } @@ -36,23 +37,31 @@ impl Kitty { if let Some(first) = it.next() { write!( buf, - "\x1b_Ga=T,f={},s={},v={},m={};{}\x1b\\", + "{}_Ga=T,f={},s={},v={},m={};{}{}\\{}", + START, format, size.0, size.1, it.peek().is_some() as u8, first.iter().collect::(), + ESCAPE, + CLOSE )?; } while let Some(chunk) = it.next() { write!( buf, - "\x1b_Gm={};{}\x1b\\", + "{}_Gm={};{}{}\\{}", + START, it.peek().is_some() as u8, - chunk.iter().collect::() + chunk.iter().collect::(), + ESCAPE, + CLOSE )?; } + + buf.write_all(CLOSE.as_bytes())?; Ok(buf) } diff --git a/adaptor/src/lib.rs b/adaptor/src/lib.rs index ed8c1a21..902f78fe 100644 --- a/adaptor/src/lib.rs +++ b/adaptor/src/lib.rs @@ -7,17 +7,28 @@ mod kitty; mod sixel; mod ueberzug; +use adaptor::*; use iterm2::*; use kitty::*; +use shared::RoCell; use sixel::*; -use ueberzug::*; -pub use crate::{adaptor::*, image::*}; +pub use crate::image::*; + +pub static ADAPTOR: RoCell = RoCell::new(); + +// Tmux support +static TMUX: RoCell = RoCell::new(); +static ESCAPE: RoCell<&'static str> = RoCell::new(); +static START: RoCell<&'static str> = RoCell::new(); +static CLOSE: RoCell<&'static str> = RoCell::new(); pub fn init() { - UEBERZUG.init(if config::PREVIEW.adaptor.needs_ueberzug() { - Ueberzug::start().ok() - } else { - None - }); + TMUX.init(std::env::var_os("TMUX").is_some()); + START.init(if *TMUX { "\x1bPtmux;\x1b\x1b" } else { "\x1b" }); + CLOSE.init(if *TMUX { "\x1b\\" } else { "" }); + ESCAPE.init(if *TMUX { "\x1b\x1b" } else { "\x1b" }); + + ADAPTOR.init(Adaptor::detect()); + ADAPTOR.start(); } diff --git a/adaptor/src/sixel.rs b/adaptor/src/sixel.rs index 7353460e..fdf957bd 100644 --- a/adaptor/src/sixel.rs +++ b/adaptor/src/sixel.rs @@ -6,7 +6,7 @@ use image::DynamicImage; use ratatui::prelude::Rect; use shared::Term; -use crate::Image; +use crate::{Image, CLOSE, ESCAPE, START}; pub(super) struct Sixel; @@ -41,7 +41,7 @@ impl Sixel { let nq = NeuQuant::new(10, 256 - alpha as usize, &img); let mut buf: Vec = Vec::with_capacity(1 << 16); - write!(buf, "\x1bP0;1;8q\"1;1;{};{}", img.width(), img.height())?; + write!(buf, "{}P0;1;8q\"1;1;{};{}", START, img.width(), img.height())?; // Palette for (i, c) in nq.color_map_rgba().chunks(4).enumerate() { @@ -90,7 +90,7 @@ impl Sixel { } } - write!(buf, "\x1b\\")?; + write!(buf, "{}\\{}", ESCAPE, CLOSE)?; Ok(buf) }) .await? diff --git a/adaptor/src/ueberzug.rs b/adaptor/src/ueberzug.rs index 4049d75b..27d891b6 100644 --- a/adaptor/src/ueberzug.rs +++ b/adaptor/src/ueberzug.rs @@ -1,15 +1,16 @@ use std::{path::PathBuf, process::Stdio}; use anyhow::Result; -use config::PREVIEW; use ratatui::prelude::Rect; use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}}; +use crate::Adaptor; + pub(super) struct Ueberzug; impl Ueberzug { - pub(super) fn start() -> Result>> { - let mut child = Self::create_demon().ok(); + pub(super) fn start(adaptor: Adaptor) -> Result>> { + let mut child = Self::create_demon(adaptor).ok(); let (tx, mut rx) = mpsc::unbounded_channel(); tokio::spawn(async move { @@ -19,7 +20,7 @@ impl Ueberzug { child = None; } if child.is_none() { - child = Self::create_demon().ok(); + child = Self::create_demon(adaptor).ok(); } if let Some(c) = &mut child { Self::send_command(c, cmd).await.ok(); @@ -30,10 +31,10 @@ impl Ueberzug { Ok(tx) } - fn create_demon() -> Result { + fn create_demon(adaptor: Adaptor) -> Result { Ok( Command::new("ueberzug") - .args(["layer", "-so", &PREVIEW.adaptor.to_string()]) + .args(["layer", "-so", &adaptor.to_string()]) .kill_on_drop(true) .stdin(Stdio::piped()) .stderr(Stdio::null()) diff --git a/config/src/preview/adaptor.rs b/config/src/preview/adaptor.rs deleted file mode 100644 index ebbcc865..00000000 --- a/config/src/preview/adaptor.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::env; - -#[derive(Debug, PartialEq, Eq)] -pub enum PreviewAdaptor { - Kitty, - Iterm2, - Sixel, - - // Supported by Überzug++ - X11, - Wayland, - Chafa, -} - -impl Default for PreviewAdaptor { - fn default() -> Self { - if env::var("KITTY_WINDOW_ID").is_ok() { - return Self::Kitty; - } - if env::var("KONSOLE_VERSION").is_ok() { - return Self::Kitty; - } - match env::var("TERM").unwrap_or_default().as_str() { - "xterm-kitty" => return Self::Kitty, - "foot" => return Self::Sixel, - _ => {} - } - match env::var("TERM_PROGRAM").unwrap_or_default().as_str() { - "iTerm.app" => return Self::Iterm2, - "WezTerm" => return cfg!(windows).then_some(Self::Iterm2).unwrap_or(Self::Kitty), - "BlackBox" => return Self::Sixel, - "vscode" => return Self::Sixel, - "Hyper" => return Self::Sixel, - "mintty" => return Self::Iterm2, - _ => {} - } - match env::var("XDG_SESSION_TYPE").unwrap_or_default().as_str() { - "x11" => Self::X11, - "wayland" => Self::Wayland, - _ => Self::Chafa, - } - } -} - -impl ToString for PreviewAdaptor { - fn to_string(&self) -> String { - match self { - PreviewAdaptor::Kitty => "kitty", - PreviewAdaptor::Iterm2 => "iterm2", - PreviewAdaptor::Sixel => "sixel", - PreviewAdaptor::X11 => "x11", - PreviewAdaptor::Wayland => "wayland", - PreviewAdaptor::Chafa => "chafa", - } - .to_string() - } -} - -impl PreviewAdaptor { - #[inline] - pub fn needs_ueberzug(&self) -> bool { - !matches!(self, PreviewAdaptor::Kitty | PreviewAdaptor::Iterm2 | PreviewAdaptor::Sixel) - } -} diff --git a/config/src/preview/mod.rs b/config/src/preview/mod.rs index 612b0d97..eacd60e4 100644 --- a/config/src/preview/mod.rs +++ b/config/src/preview/mod.rs @@ -1,5 +1,3 @@ -mod adaptor; mod preview; -pub use adaptor::*; pub use preview::*; diff --git a/config/src/preview/preview.rs b/config/src/preview/preview.rs index 6bde131e..7dfdfad6 100644 --- a/config/src/preview/preview.rs +++ b/config/src/preview/preview.rs @@ -4,13 +4,10 @@ use md5::{Digest, Md5}; use serde::Deserialize; use shared::expand_path; -use super::PreviewAdaptor; use crate::{xdg::Xdg, MERGED_YAZI}; #[derive(Debug)] pub struct Preview { - pub adaptor: PreviewAdaptor, - pub tab_size: u32, pub max_width: u32, pub max_height: u32, @@ -39,8 +36,6 @@ impl Default for Preview { preview.cache_dir.filter(|p| !p.is_empty()).map_or_else(Xdg::cache_dir, expand_path); Preview { - adaptor: Default::default(), - tab_size: preview.tab_size, max_width: preview.max_width, max_height: preview.max_height, diff --git a/core/src/manager/preview/preview.rs b/core/src/manager/preview/preview.rs index 11a8c1b8..eebd869d 100644 --- a/core/src/manager/preview/preview.rs +++ b/core/src/manager/preview/preview.rs @@ -1,6 +1,6 @@ use std::{sync::atomic::Ordering, time::Duration}; -use adaptor::Adaptor; +use adaptor::ADAPTOR; use config::MANAGER; use shared::{MimeKind, PeekError, Url, MIME_DIR}; use tokio::{pin, task::JoinHandle}; @@ -153,7 +153,7 @@ impl Preview { pub fn reset bool>(&mut self, f: F) -> bool { self.handle.take().map(|h| h.abort()); INCR.fetch_add(1, Ordering::Relaxed); - Adaptor::image_hide(MANAGER.layout.preview_rect()).ok(); + ADAPTOR.image_hide(MANAGER.layout.preview_rect()).ok(); let Some(ref lock) = self.lock else { return false; diff --git a/core/src/manager/preview/provider.rs b/core/src/manager/preview/provider.rs index cf54a127..4ac499db 100644 --- a/core/src/manager/preview/provider.rs +++ b/core/src/manager/preview/provider.rs @@ -1,6 +1,6 @@ use std::{io::BufRead, path::Path, sync::atomic::{AtomicUsize, Ordering}}; -use adaptor::Adaptor; +use adaptor::ADAPTOR; use anyhow::anyhow; use config::{MANAGER, PREVIEW}; use shared::{MimeKind, PeekError}; @@ -46,7 +46,7 @@ impl Provider { } pub(super) async fn image(path: &Path) -> Result { - Adaptor::image_show(path, MANAGER.layout.preview_rect()).await?; + ADAPTOR.image_show(path, MANAGER.layout.preview_rect()).await?; Ok(PreviewData::Image) } diff --git a/shared/src/ro_cell.rs b/shared/src/ro_cell.rs index 6b0b8dc4..cf2bd575 100644 --- a/shared/src/ro_cell.rs +++ b/shared/src/ro_cell.rs @@ -1,4 +1,4 @@ -use std::{cell::UnsafeCell, ops::Deref}; +use std::{cell::UnsafeCell, fmt::{self, Display}, ops::Deref}; // Read-only cell. It's safe to use this in a static variable, but it's not safe // to mutate it. This is useful for storing static data that is expensive to @@ -32,3 +32,10 @@ impl Deref for RoCell { fn deref(&self) -> &Self::Target { unsafe { (*self.0.get()).as_ref().unwrap() } } } + +impl Display for RoCell +where + T: Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.deref().fmt(f) } +}