mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: support for image preview within tmux (#147)
This commit is contained in:
parent
e6c236c179
commit
e6fccf9d17
13 changed files with 167 additions and 116 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<Option<UnboundedSender<Option<(PathBuf, Rect)>>>> =
|
||||
RoCell::new();
|
||||
static UEBERZUG: RoCell<Option<UnboundedSender<Option<(PathBuf, Rect)>>>> = 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<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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::<String>(),
|
||||
ESCAPE,
|
||||
CLOSE
|
||||
)?;
|
||||
}
|
||||
|
||||
while let Some(chunk) = it.next() {
|
||||
write!(
|
||||
buf,
|
||||
"\x1b_Gm={};{}\x1b\\",
|
||||
"{}_Gm={};{}{}\\{}",
|
||||
START,
|
||||
it.peek().is_some() as u8,
|
||||
chunk.iter().collect::<String>()
|
||||
chunk.iter().collect::<String>(),
|
||||
ESCAPE,
|
||||
CLOSE
|
||||
)?;
|
||||
}
|
||||
|
||||
buf.write_all(CLOSE.as_bytes())?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Adaptor> = RoCell::new();
|
||||
|
||||
// Tmux support
|
||||
static TMUX: RoCell<bool> = 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<u8> = 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?
|
||||
|
|
|
|||
|
|
@ -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<UnboundedSender<Option<(PathBuf, Rect)>>> {
|
||||
let mut child = Self::create_demon().ok();
|
||||
pub(super) fn start(adaptor: Adaptor) -> Result<UnboundedSender<Option<(PathBuf, Rect)>>> {
|
||||
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<Child> {
|
||||
fn create_demon(adaptor: Adaptor) -> Result<Child> {
|
||||
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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
mod adaptor;
|
||||
mod preview;
|
||||
|
||||
pub use adaptor::*;
|
||||
pub use preview::*;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<F: FnOnce(&PreviewLock) -> 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;
|
||||
|
|
|
|||
|
|
@ -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<PreviewData, PeekError> {
|
||||
Adaptor::image_show(path, MANAGER.layout.preview_rect()).await?;
|
||||
ADAPTOR.image_show(path, MANAGER.layout.preview_rect()).await?;
|
||||
Ok(PreviewData::Image)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<T> Deref for RoCell<T> {
|
|||
|
||||
fn deref(&self) -> &Self::Target { unsafe { (*self.0.get()).as_ref().unwrap() } }
|
||||
}
|
||||
|
||||
impl<T> Display for RoCell<T>
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.deref().fmt(f) }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue