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) }
+}